From b843d024567289e1ff6ca8e4837d62a218fa568f Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 29 Aug 2022 08:21:10 -0700 Subject: [PATCH 001/455] Begin adding vector amplitude reg --- SimPEG/regularization/__init__.py | 1 + SimPEG/regularization/regularization_mesh.py | 26 +- SimPEG/regularization/sparse.py | 18 +- SimPEG/regularization/vector_amplitude.py | 162 +++++++ .../plot_inv_mag_MVI_VectorAmplitude.py | 410 ++++++++++++++++++ 5 files changed, 596 insertions(+), 21 deletions(-) create mode 100644 SimPEG/regularization/vector_amplitude.py create mode 100644 examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 4eb52f4885..99ff9d5b43 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -13,6 +13,7 @@ from .cross_gradient import CrossGradient from .correspondence import LinearCorrespondence from .jtv import JointTotalVariation +from .vector_amplitude import VectorAmplitude @deprecate_class(removal_version="0.x.0", future_warn=True) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index ce915d2c56..9f6602e9be 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -33,7 +33,7 @@ def __init__(self, mesh, active_cells=None, **kwargs): utils.setKwargs(self, **kwargs) @property - def active_cells(self) -> scipy.sparse.csr_matrix: + def active_cells(self) -> sp.csr_matrix: """A boolean array indicating whether a cell is active Notes @@ -132,7 +132,7 @@ def dim(self) -> int: return self.mesh.dim @property - def Pac(self) -> scipy.sparse.csr_matrix: + def Pac(self) -> sp.csr_matrix: """ Projection matrix that takes from the reduced space of active cells to full modelling space (ie. nC x nactive_cells). @@ -145,7 +145,7 @@ def Pac(self) -> scipy.sparse.csr_matrix: return self._Pac @property - def Pafx(self) -> scipy.sparse.csr_matrix: + def Pafx(self) -> sp.csr_matrix: """ Projection matrix that takes from the reduced space of active x-faces to full modelling space (ie. nFx x nactive_cells_Fx ) @@ -168,7 +168,7 @@ def Pafx(self) -> scipy.sparse.csr_matrix: return self._Pafx @property - def Pafy(self) -> scipy.sparse.csr_matrix: + def Pafy(self) -> sp.csr_matrix: """ Projection matrix that takes from the reduced space of active y-faces to full modelling space (ie. nFy x nactive_cells_Fy ). @@ -193,7 +193,7 @@ def Pafy(self) -> scipy.sparse.csr_matrix: return self._Pafy @property - def Pafz(self) -> scipy.sparse.csr_matrix: + def Pafz(self) -> sp.csr_matrix: """ Projection matrix that takes from the reduced space of active z-faces to full modelling space (ie. nFz x nactive_cells_Fz ). @@ -216,7 +216,7 @@ def Pafz(self) -> scipy.sparse.csr_matrix: return self._Pafz @property - def average_face_to_cell(self) -> scipy.sparse.csr_matrix: + def average_face_to_cell(self) -> sp.csr_matrix: """ Vertically stacked matrix of cell averaging operators from active cell centers to active faces along each dimension of the mesh. @@ -229,7 +229,7 @@ def average_face_to_cell(self) -> scipy.sparse.csr_matrix: return sp.hstack([self.aveFx2CC, self.aveFy2CC, self.aveFz2CC]) @property - def aveFx2CC(self) -> scipy.sparse.csr_matrix: + def aveFx2CC(self) -> sp.csr_matrix: """ Averaging from active cell centers to active x-faces. """ @@ -244,7 +244,7 @@ def aveFx2CC(self) -> scipy.sparse.csr_matrix: return self._aveFx2CC @property - def aveCC2Fx(self) -> scipy.sparse.csr_matrix: + def aveCC2Fx(self) -> sp.csr_matrix: """ Averaging from active x-faces to active cell centers. """ @@ -260,7 +260,7 @@ def aveCC2Fx(self) -> scipy.sparse.csr_matrix: return self._aveCC2Fx @property - def aveFy2CC(self) -> scipy.sparse.csr_matrix: + def aveFy2CC(self) -> sp.csr_matrix: """ Averaging from active cell centers to active y-faces. """ @@ -277,7 +277,7 @@ def aveFy2CC(self) -> scipy.sparse.csr_matrix: return self._aveFy2CC @property - def aveCC2Fy(self) -> scipy.sparse.csr_matrix: + def aveCC2Fy(self) -> sp.csr_matrix: """ Averaging matrix from active y-faces to active cell centers. """ @@ -295,7 +295,7 @@ def aveCC2Fy(self) -> scipy.sparse.csr_matrix: return self._aveCC2Fy @property - def aveFz2CC(self) -> scipy.sparse.csr_matrix: + def aveFz2CC(self) -> sp.csr_matrix: """ Averaging from active cell centers to active z-faces. """ @@ -310,7 +310,7 @@ def aveFz2CC(self) -> scipy.sparse.csr_matrix: return self._aveFz2CC @property - def aveCC2Fz(self) -> scipy.sparse.csr_matrix: + def aveCC2Fz(self) -> sp.csr_matrix: """ Averaging matrix from active z-faces to active cell centers. """ @@ -333,7 +333,7 @@ def base_length(self) -> float: return self._base_length @property - def cell_gradient(self) -> scipy.sparse.csr_matrix: + def cell_gradient(self) -> sp.csr_matrix: """ Vertically stacked matrix of cell gradients along each dimension of the mesh. diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 163f418fa4..ac0356a246 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -225,6 +225,7 @@ def __init__( gradient_type="total", irls_scaled=True, irls_threshold=1e-8, + objfcts=None, **kwargs, ): if not isinstance(mesh, RegularizationMesh): @@ -239,16 +240,17 @@ def __init__( if active_cells is not None: self._regularization_mesh.active_cells = active_cells - objfcts = [ - SparseSmall(mesh=self.regularization_mesh), - SparseDeriv(mesh=self.regularization_mesh, orientation="x"), - ] + if objfcts is None: + objfcts = [ + SparseSmall(mesh=self.regularization_mesh), + SparseDeriv(mesh=self.regularization_mesh, orientation="x"), + ] - if mesh.dim > 1: - objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="y")) + if mesh.dim > 1: + objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="y")) - if mesh.dim > 2: - objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="z")) + if mesh.dim > 2: + objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="z")) gradientType = kwargs.pop("gradientType", None) super().__init__( diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py new file mode 100644 index 0000000000..1343548b68 --- /dev/null +++ b/SimPEG/regularization/vector_amplitude.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import numpy as np +import scipy.sparse as sp +from typing import TYPE_CHECKING + +from discretize.base import BaseMesh +from SimPEG import maps +from .base import ( + RegularizationMesh, + BaseRegularization +) +from .sparse import ( + Sparse, + SparseSmall, + SparseDeriv +) +from .. import utils + +if TYPE_CHECKING: + from scipy.sparse import csr_matrix + + +class BaseVectorAmplitude(BaseRegularization): + """ + Base vector amplitude function. + """ + _projection = None + + @property + def projection(self): + """Projection matrix from vector components to amplitude.""" + if getattr(self, "_projection", None) is None: + self._projection = sp.hstack([ + sp.identity(self.nP), + sp.identity(self.nP), + sp.identity(self.nP) + ]) + + return self._projection + + def amplitude_map(self, m): + """Create sparse vector model.""" + return self.projection @ utils.sdiag(m) + + @property + def mapping(self) -> maps.IdentityMap: + """Mapping applied to the model values""" + return self._mapping + + @mapping.setter + def mapping(self, mapping: maps.IdentityMap): + if mapping is None: + mapping = maps.IdentityMap() + if not isinstance(mapping, maps.IdentityMap): + raise TypeError( + f"'mapping' must be of type {maps.IdentityMap}. " + f"Value of type {type(mapping)} provided." + ) + self._mapping = mapping + + +class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): + """ + Sparse smallness regularization on vector amplitude. + + **Inputs** + + :param int norm: norm on the smallness + """ + + def f_m(self, m): + """ + Compute the amplitude of a vector model. + """ + m = self.amplitude_map(m) * m + return self.mapping * self._delta_m(m) + + def f_m_deriv(self, m) -> csr_matrix: + m = self.amplitude_map(m) * m + + return self.mapping.deriv(self._delta_m(m)) @ self.projection + + +class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): + """ + Base Class for sparse regularization on first spatial derivatives + """ + + def f_m(self, m): + m = self.amplitude_map(m) * m + dfm_dl = self.cell_gradient @ (self._delta_m(m)) + + return dfm_dl + + def f_m_deriv(self, m) -> csr_matrix: + m = self.amplitude_map(m) * m + return (self.cell_gradient @ self.mapping.deriv(self._delta_m(m))) @ self.projection + + +class VectorAmplitude(Sparse): + """ + The regularization is: + + .. math:: + + R(m) = \\frac{1}{2}\\mathbf{(m-m_\\text{ref})^\\top W^\\top R^\\top R + W(m-m_\\text{ref})} + + where the IRLS weight + + .. math:: + + R = \\eta TO FINISH LATER!!! + + So the derivative is straight forward: + + .. math:: + + R(m) = \\mathbf{W^\\top R^\\top R W (m-m_\\text{ref})} + + The IRLS weights are recomputed after each beta solves. + It is strongly recommended to do a few Gauss-Newton iterations + before updating. + """ + + def __init__( + self, + mesh, + active_cells=None, + **kwargs, + ): + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + + if not isinstance(mesh, RegularizationMesh): + TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + f"Value of type {type(mesh)} provided." + ) + self._regularization_mesh = mesh + + if active_cells is not None: + self._regularization_mesh.active_cells = active_cells + + objfcts = [ + VectorAmplitudeSmall(mesh=self.regularization_mesh), + VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="x"), + ] + + if mesh.dim > 1: + objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="y")) + + if mesh.dim > 2: + objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="z")) + + super().__init__( + self.regularization_mesh, + objfcts=objfcts, + **kwargs, + ) + diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py new file mode 100644 index 0000000000..efbc7fa609 --- /dev/null +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -0,0 +1,410 @@ +""" +Magnetic inversion on a TreeMesh +================================ + +In this example, we demonstrate the use of a Magnetic Vector Inverison +on 3D TreeMesh for the inversion of magnetic data. + +The inverse problem uses the :class:'SimPEG.regularization.VectorAmplitude' +regularization borrowed from ... + +""" + +from discretize import TreeMesh +from SimPEG import ( + data, + data_misfit, + directives, + maps, + inverse_problem, + optimization, + inversion, + regularization, +) + +from SimPEG import utils +from SimPEG.utils import mkvc + +from discretize.utils import mesh_builder_xyz, refine_tree_xyz +from SimPEG.potential_fields import magnetics +import scipy as sp +import numpy as np +import matplotlib.pyplot as plt + + +# sphinx_gallery_thumbnail_number = 3 + +############################################################################### +# Setup +# ----- +# +# Define the survey and model parameters +# +# First we need to define the direction of the inducing field +# As a simple case, we pick a vertical inducing field of magnitude 50,000 nT. +# +# +np.random.seed(1) +# We will assume a vertical inducing field +H0 = (50000.0, 90.0, 0.0) + +# The magnetization is set along a different direction (induced + remanence) +M = np.array([45.0, 90.0]) + +# Create grid of points for topography +# Lets create a simple Gaussian topo and set the active cells +[xx, yy] = np.meshgrid(np.linspace(-200, 200, 50), np.linspace(-200, 200, 50)) +b = 100 +A = 50 +zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0)) + +topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + +# Create an array of observation points +xr = np.linspace(-100.0, 100.0, 20) +yr = np.linspace(-100.0, 100.0, 20) +X, Y = np.meshgrid(xr, yr) +Z = A * np.exp(-0.5 * ((X / b) ** 2.0 + (Y / b) ** 2.0)) + 5 + +# Create a MAGsurvey +xyzLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] +rxLoc = magnetics.receivers.Point(xyzLoc) +srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) +survey = magnetics.survey.Survey(srcField) + +# Here how the topography looks with a quick interpolation, just a Gaussian... +tri = sp.spatial.Delaunay(topo) +fig = plt.figure() +ax = fig.add_subplot(1, 1, 1, projection="3d") +ax.plot_trisurf( + topo[:, 0], topo[:, 1], topo[:, 2], triangles=tri.simplices, cmap=plt.cm.Spectral +) +ax.scatter3D(xyzLoc[:, 0], xyzLoc[:, 1], xyzLoc[:, 2], c="k") +plt.show() + +############################################################################### +# Inversion Mesh +# -------------- +# +# Here, we create a TreeMesh with base cell size of 5 m. We created a small +# utility function to center the mesh around points and to figure out the +# outermost dimension for adequate padding distance. +# The second stage allows us to refine the mesh around points or surfaces +# (point assumed to follow some horizontal trend) +# The refinement process is repeated twice to allow for a finer level around +# the survey locations. +# + +# Create a mesh +h = [10, 10, 10] +padDist = np.ones((3, 2)) * 100 + +mesh = mesh_builder_xyz( + xyzLoc, h, padding_distance=padDist, depth_core=100, mesh_type="tree" +) +mesh = refine_tree_xyz( + mesh, topo, method="surface", octree_levels=[4, 4], finalize=True +) + + +# Define an active cells from topo +actv = utils.surface2ind_topo(mesh, topo) +nC = int(actv.sum()) + +########################################################################### +# A simple function to plot vectors in TreeMesh +# +# Should eventually end up on discretize +# + + +def plotVectorSectionsOctree( + mesh, + m, + normal="X", + ind=0, + vmin=None, + vmax=None, + scale=1.0, + vec="k", + axs=None, + actvMap=None, + fill=True, +): + + """ + Plot section through a 3D tensor model + """ + # plot recovered model + normalInd = {"X": 0, "Y": 1, "Z": 2}[normal] + antiNormalInd = {"X": [1, 2], "Y": [0, 2], "Z": [0, 1]}[normal] + + h2d = (mesh.h[antiNormalInd[0]], mesh.h[antiNormalInd[1]]) + x2d = (mesh.x0[antiNormalInd[0]], mesh.x0[antiNormalInd[1]]) + + #: Size of the sliced dimension + szSliceDim = len(mesh.h[normalInd]) + if ind is None: + ind = int(szSliceDim // 2) + + cc_tensor = [None, None, None] + for i in range(3): + cc_tensor[i] = np.cumsum(np.r_[mesh.x0[i], mesh.h[i]]) + cc_tensor[i] = (cc_tensor[i][1:] + cc_tensor[i][:-1]) * 0.5 + slice_loc = cc_tensor[normalInd][ind] + + # Create a temporary TreeMesh with the slice through + temp_mesh = TreeMesh(h2d, x2d) + level_diff = mesh.max_level - temp_mesh.max_level + + XS = [None, None, None] + XS[antiNormalInd[0]], XS[antiNormalInd[1]] = np.meshgrid( + cc_tensor[antiNormalInd[0]], cc_tensor[antiNormalInd[1]] + ) + XS[normalInd] = np.ones_like(XS[antiNormalInd[0]]) * slice_loc + loc_grid = np.c_[XS[0].reshape(-1), XS[1].reshape(-1), XS[2].reshape(-1)] + inds = np.unique(mesh._get_containing_cell_indexes(loc_grid)) + + grid2d = mesh.gridCC[inds][:, antiNormalInd] + levels = mesh._cell_levels_by_indexes(inds) - level_diff + temp_mesh.insert_cells(grid2d, levels) + tm_gridboost = np.empty((temp_mesh.nC, 3)) + tm_gridboost[:, antiNormalInd] = temp_mesh.gridCC + tm_gridboost[:, normalInd] = slice_loc + + # Interpolate values to mesh.gridCC if not 'CC' + mx = actvMap * m[:, 0] + my = actvMap * m[:, 1] + mz = actvMap * m[:, 2] + + m = np.c_[mx, my, mz] + + # Interpolate values from mesh.gridCC to grid2d + ind_3d_to_2d = mesh._get_containing_cell_indexes(tm_gridboost) + v2d = m[ind_3d_to_2d, :] + amp = np.sum(v2d ** 2.0, axis=1) ** 0.5 + + if axs is None: + axs = plt.subplot(111) + + if fill: + temp_mesh.plotImage(amp, ax=axs, clim=[vmin, vmax], grid=True) + + axs.quiver( + temp_mesh.gridCC[:, 0], + temp_mesh.gridCC[:, 1], + v2d[:, antiNormalInd[0]], + v2d[:, antiNormalInd[1]], + pivot="mid", + scale_units="inches", + scale=scale, + linewidths=(1,), + edgecolors=(vec), + headaxislength=0.1, + headwidth=10, + headlength=30, + ) + + +########################################################################### +# Forward modeling data +# --------------------- +# +# We can now create a magnetization model and generate data +# Lets start with a block below topography +# + + +model = np.zeros((mesh.nC, 3)) + +# Convert the inclination declination to vector in Cartesian +M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) + +# Get the indicies of the magnetized block +ind = utils.model_builder.getIndicesBlock( + np.r_[-20, -20, -10], + np.r_[20, 20, 25], + mesh.gridCC, +)[0] + +# Assign magnetization values +model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + +# Remove air cells +model = model[actv, :] + +# Create active map to go from reduce set to full +actvMap = maps.InjectActiveCells(mesh, actv, np.nan) + +# Creat reduced identity map +idenMap = maps.IdentityMap(nP=nC * 3) + +# Create the simulation +simulation = magnetics.simulation.Simulation3DIntegral( + survey=survey, mesh=mesh, chiMap=idenMap, actInd=actv, model_type="vector" +) + +# Compute some data and add some random noise +d = simulation.dpred(mkvc(model)) +std = 5 # nT +synthetic_data = d + np.random.randn(len(d)) * std +wd = np.ones(len(d)) * std + +# Assign data and uncertainties to the survey +data_object = data.Data(survey, dobs=synthetic_data, standard_deviation=wd) + +# Create an projection matrix for plotting later +actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) + +# Plot the model and data +plt.figure() +ax = plt.subplot(2, 1, 1) +im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) +plt.colorbar(im[0]) +ax.set_title("Predicted data.") +plt.gca().set_aspect("equal", adjustable="box") + +# Plot the vector model +ax = plt.subplot(2, 1, 2) +plotVectorSectionsOctree( + mesh, + model, + axs=ax, + normal="Y", + # ind=66, + actvMap=actv_plot, + scale=0.5, + vmin=0.0, + vmax=0.025, +) +ax.set_xlim([-200, 200]) +ax.set_ylim([-100, 75]) +ax.set_xlabel("x") +ax.set_ylabel("y") +plt.gca().set_aspect("equal", adjustable="box") + +plt.show() + + +###################################################################### +# Inversion +# --------- +# +# We can now attempt the inverse calculations. We put great care +# into designing an inversion methology that would yield a geologically +# reasonable solution for the non-induced problem. +# The inversion is done in two stages. First we compute a smooth +# solution using a Cartesian coordinate system, then a sparse +# inversion in the Spherical domain. +# + +# Create sensitivity weights from our linear forward operator +rxLoc = survey.source_field.receiver_list[0].locations + +# This Mapping connects the regularizations for the three-component +# vector model +wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) + + +m0 = np.ones(3 * nC) * 1e-4 # Starting model + +# Create three regularizations for the different components +# of magnetization +reg = regularization.VectorAmplitude(mesh, active_cells=actv) +reg.reference_model = np.zeros(nC) + +# Data misfit function +dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) +dmis.W = 1.0 / data_object.standard_deviation + +# Add directives to the inversion +opt = optimization.ProjectedGNCG( + maxIter=10, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 +) + +invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) + +# A list of directive to control the inverson +betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) + +# Add sensitivity weights +sensitivity_weights = directives.UpdateSensitivityWeights() + +# Here is where the norms are applied +# Use a threshold parameter empirically based on the distribution of +# model parameters +IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) + +# Pre-conditioner +update_Jacobi = directives.UpdatePreconditioner() + +inv = inversion.BaseInversion( + invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] +) + +# Run the inversion +mrec_MVIC = inv.run(m0) + + +############################################################# +# Final Plot +# ---------- +# +# Let's compare the smooth and compact model +# +# +# + +plt.figure(figsize=(8, 8)) +ax = plt.subplot(2, 1, 1) +plotVectorSectionsOctree( + mesh, + mrec_MVIC.reshape((nC, 3), order="F"), + axs=ax, + normal="Y", + ind=65, + actvMap=actv_plot, + scale=0.05, + vmin=0.0, + vmax=0.005, +) +ax.set_xlim([-200, 200]) +ax.set_ylim([-100, 75]) +ax.set_title("Smooth model (Cartesian)") +ax.set_xlabel("x") +ax.set_ylabel("y") +plt.gca().set_aspect("equal", adjustable="box") + +ax = plt.subplot(2, 1, 2) + +plotVectorSectionsOctree( + mesh, + opt.l2model.reshape((nC, 3), order="F"), + axs=ax, + normal="Y", + ind=65, + actvMap=actv_plot, + scale=0.4, + vmin=0.0, + vmax=0.025, +) +ax.set_xlim([-200, 200]) +ax.set_ylim([-100, 75]) +ax.set_title("Sparse model (Spherical)") +ax.set_xlabel("x") +ax.set_ylabel("y") +plt.gca().set_aspect("equal", adjustable="box") + +plt.show() + +# Plot the final predicted data and the residual +plt.figure() +ax = plt.subplot(1, 2, 1) +utils.plot_utils.plot2Ddata(xyzLoc, invProb.dpred, ax=ax) +ax.set_title("Predicted data.") +plt.gca().set_aspect("equal", adjustable="box") + +ax = plt.subplot(1, 2, 2) +utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data - invProb.dpred, ax=ax) +ax.set_title("Data residual.") +plt.gca().set_aspect("equal", adjustable="box") From 42850963519d0cd95a7acb79f8b8662e3c21952f Mon Sep 17 00:00:00 2001 From: fourndo Date: Tue, 30 Aug 2022 07:24:09 -0700 Subject: [PATCH 002/455] Continue work --- SimPEG/regularization/vector_amplitude.py | 45 ++++--------------- .../plot_inv_mag_MVI_VectorAmplitude.py | 5 ++- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 1343548b68..5858be15ca 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -27,37 +27,9 @@ class BaseVectorAmplitude(BaseRegularization): """ _projection = None - @property - def projection(self): - """Projection matrix from vector components to amplitude.""" - if getattr(self, "_projection", None) is None: - self._projection = sp.hstack([ - sp.identity(self.nP), - sp.identity(self.nP), - sp.identity(self.nP) - ]) - - return self._projection - def amplitude_map(self, m): """Create sparse vector model.""" - return self.projection @ utils.sdiag(m) - - @property - def mapping(self) -> maps.IdentityMap: - """Mapping applied to the model values""" - return self._mapping - - @mapping.setter - def mapping(self, mapping: maps.IdentityMap): - if mapping is None: - mapping = maps.IdentityMap() - if not isinstance(mapping, maps.IdentityMap): - raise TypeError( - f"'mapping' must be of type {maps.IdentityMap}. " - f"Value of type {type(mapping)} provided." - ) - self._mapping = mapping + return np.linalg.norm(m.reshape((self.regularization_mesh.nC, -1)), axis=1) class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): @@ -73,13 +45,12 @@ def f_m(self, m): """ Compute the amplitude of a vector model. """ - m = self.amplitude_map(m) * m - return self.mapping * self._delta_m(m) + + return self.amplitude_map(self.mapping * self._delta_m(m)) def f_m_deriv(self, m) -> csr_matrix: - m = self.amplitude_map(m) * m - return self.mapping.deriv(self._delta_m(m)) @ self.projection + return self.mapping.deriv(self._delta_m(m)) class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): @@ -88,14 +59,14 @@ class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): """ def f_m(self, m): - m = self.amplitude_map(m) * m - dfm_dl = self.cell_gradient @ (self._delta_m(m)) + m = self.amplitude_map(self.mapping * self._delta_m(m)) + dfm_dl = self.cell_gradient @ m return dfm_dl def f_m_deriv(self, m) -> csr_matrix: - m = self.amplitude_map(m) * m - return (self.cell_gradient @ self.mapping.deriv(self._delta_m(m))) @ self.projection + m = self.amplitude_map(self.mapping * self._delta_m(m)) + return self.cell_gradient @ self.mapping.deriv(m) class VectorAmplitude(Sparse): diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index efbc7fa609..5bc7aed23b 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -308,10 +308,11 @@ def plotVectorSectionsOctree( m0 = np.ones(3 * nC) * 1e-4 # Starting model +idenMap = maps.IdentityMap(nP=3*nC) # Create three regularizations for the different components # of magnetization -reg = regularization.VectorAmplitude(mesh, active_cells=actv) -reg.reference_model = np.zeros(nC) +reg = regularization.VectorAmplitude(mesh, active_cells=actv, mapping=idenMap) +reg.reference_model = np.zeros(3*nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) From 015b1271a3e23fc782b3a8b3b618ef5116be6251 Mon Sep 17 00:00:00 2001 From: fourndo Date: Tue, 30 Aug 2022 07:56:44 -0700 Subject: [PATCH 003/455] Start re-facotring to use wires --- SimPEG/regularization/base.py | 4 +- SimPEG/regularization/vector_amplitude.py | 76 ++++++++++++------- .../plot_inv_mag_MVI_VectorAmplitude.py | 3 +- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 6be52d5815..fcdbb2342b 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -272,8 +272,8 @@ def _nC_residual(self) -> int: nC = getattr(self.regularization_mesh, "nC", None) mapping = getattr(self, "_mapping", None) - if mapping is not None and mapping.shape[1] != "*": - return self.mapping.shape[1] + if mapping is not None and mapping.nP != "*": + return self.mapping.nP if nC != "*" and nC is not None: return self.regularization_mesh.nC diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 5858be15ca..3b6af7544d 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -27,9 +27,32 @@ class BaseVectorAmplitude(BaseRegularization): """ _projection = None + def __init__(self, mesh, **kwargs): + super().__init__(mesh, **kwargs) + + @property + def mapping(self): + return self._mapping + + @mapping.setter + def mapping(self, wires): + if not isinstance(wires, maps.Wires): + raise ValueError(f"A 'mapping' of type {maps.Wires} must be provided.") + + for wire in wires.maps: + if wire[1].shape[0] != self.regularization_mesh.nC: + raise ValueError( + f"All models must be the same size! Got {wire} with shape{wire[1].shape[0]}" + ) + self._mapping = wires + def amplitude_map(self, m): """Create sparse vector model.""" - return np.linalg.norm(m.reshape((self.regularization_mesh.nC, -1)), axis=1) + amplitude = 0 + for wire_model in (self.mapping * m): + amplitude += wire_model**2. + + return amplitude**0.5 class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): @@ -72,32 +95,13 @@ def f_m_deriv(self, m) -> csr_matrix: class VectorAmplitude(Sparse): """ The regularization is: - - .. math:: - - R(m) = \\frac{1}{2}\\mathbf{(m-m_\\text{ref})^\\top W^\\top R^\\top R - W(m-m_\\text{ref})} - - where the IRLS weight - - .. math:: - - R = \\eta TO FINISH LATER!!! - - So the derivative is straight forward: - - .. math:: - - R(m) = \\mathbf{W^\\top R^\\top R W (m-m_\\text{ref})} - - The IRLS weights are recomputed after each beta solves. - It is strongly recommended to do a few Gauss-Newton iterations - before updating. + ... """ def __init__( self, mesh, + wire_map, active_cells=None, **kwargs, ): @@ -115,19 +119,39 @@ def __init__( self._regularization_mesh.active_cells = active_cells objfcts = [ - VectorAmplitudeSmall(mesh=self.regularization_mesh), - VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="x"), + VectorAmplitudeSmall(mesh=self.regularization_mesh, mapping=wire_map), + VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="x"), ] if mesh.dim > 1: - objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="y")) + objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="y")) if mesh.dim > 2: - objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, orientation="z")) + objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="z")) super().__init__( self.regularization_mesh, objfcts=objfcts, + mapping=wire_map, **kwargs, ) + @property + def mapping(self): + return self._mapping + + @mapping.setter + def mapping(self, wires): + if not isinstance(wires, maps.Wires): + raise ValueError(f"A 'mapping' of type {maps.Wires} must be provided.") + + for wire in wires.maps: + if wire[1].shape[0] != self.regularization_mesh.nC: + raise ValueError( + f"All models must be the same size! Got {wire} with shape{wire[1].shape[0]}" + ) + self._mapping = wires + + for fct in self.objfcts: + fct.mapping = wires + diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 5bc7aed23b..e13a7a8bd1 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -308,10 +308,9 @@ def plotVectorSectionsOctree( m0 = np.ones(3 * nC) * 1e-4 # Starting model -idenMap = maps.IdentityMap(nP=3*nC) # Create three regularizations for the different components # of magnetization -reg = regularization.VectorAmplitude(mesh, active_cells=actv, mapping=idenMap) +reg = regularization.VectorAmplitude(mesh, active_cells=actv, wire_map=wires) reg.reference_model = np.zeros(3*nC) # Data misfit function From 85cf6d74a8ee459efff54a91c0453a90ea9dd33b Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 1 Sep 2022 09:57:34 -0700 Subject: [PATCH 004/455] Fix issue with units is None --- SimPEG/regularization/base.py | 4 ++-- SimPEG/regularization/sparse.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index fcdbb2342b..bf1c3ddfc9 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -532,7 +532,7 @@ def f_m(self, m): """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) - if self.units.lower() == "radian": + if self.units is not None and self.units.lower() == "radian": return ( utils.mat_utils.coterminal(dfm_dl * self._cell_distances) / self._cell_distances @@ -602,7 +602,7 @@ def f_m(self, m): """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) - if self.units.lower() == "radian": + if self.units is not None and self.units.lower() == "radian": dfm_dl = ( utils.mat_utils.coterminal(dfm_dl * self.length_scales) / self.length_scales diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index ac0356a246..803c957ba8 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -154,7 +154,7 @@ def update_weights(self, m): * delta_m ) - if self.units.lower() == "radian": + if self.units is not None and self.units.lower() == "radian": dm = utils.mat_utils.coterminal(dm) dm_dl = dm / length_scales From 174f3829c5e34470462a50eba6205da3bee94bd0 Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 1 Sep 2022 09:57:59 -0700 Subject: [PATCH 005/455] Attempts to do Amplitude from mapping --- SimPEG/maps.py | 31 +++++++++++++++++++ .../plot_inv_mag_MVI_VectorAmplitude.py | 10 +++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index bc1ee3e6cf..8ce7622d1c 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -649,6 +649,37 @@ def nP(self): return self._nP +class Amplitude(Wires, IdentityMap): + def __mul__(self, val): + assert isinstance(val, np.ndarray) + amplitude = np.linalg.norm(np.c_[[w * val for _, w in self.maps]], axis=0) + return amplitude + + def deriv(self, m, v=None): + """ + :param numpy.ndarray m: model + :rtype: scipy.sparse.csr_matrix + :return: derivative of transformed model + """ + deriv = 0 + for _, map in self.maps: + deriv += map.deriv(m, v) + + return deriv + + def deriv(self, m, v=None): + """ + :param numpy.ndarray m: model + :rtype: scipy.sparse.csr_matrix + :return: derivative of transformed model + """ + deriv = 0 + for _, map in self.maps: + deriv += map.deriv(m, v) + + return deriv + + class SelfConsistentEffectiveMedium(IdentityMap, properties.HasProperties): """ Two phase self-consistent effective medium theory mapping for diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index e13a7a8bd1..92b1034561 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -96,7 +96,7 @@ # # Create a mesh -h = [10, 10, 10] +h = [5, 5, 5] padDist = np.ones((3, 2)) * 100 mesh = mesh_builder_xyz( @@ -271,7 +271,7 @@ def plotVectorSectionsOctree( model, axs=ax, normal="Y", - # ind=66, + ind=66, actvMap=actv_plot, scale=0.5, vmin=0.0, @@ -303,14 +303,14 @@ def plotVectorSectionsOctree( # This Mapping connects the regularizations for the three-component # vector model -wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) +wires = maps.Amplitude(("p", nC), ("s", nC), ("t", nC)) m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components # of magnetization -reg = regularization.VectorAmplitude(mesh, active_cells=actv, wire_map=wires) +reg = regularization.Sparse(mesh, active_cells=actv, mapping=wires) reg.reference_model = np.zeros(3*nC) # Data misfit function @@ -379,7 +379,7 @@ def plotVectorSectionsOctree( plotVectorSectionsOctree( mesh, - opt.l2model.reshape((nC, 3), order="F"), + invProb.l2model.reshape((nC, 3), order="F"), axs=ax, normal="Y", ind=65, From b4efd7f7cec738f28659b68b42e72ec917b532ea Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 12 Sep 2022 08:56:22 -0700 Subject: [PATCH 006/455] Temp progress --- SimPEG/directives/directives.py | 22 +++- SimPEG/maps.py | 33 +++-- SimPEG/regularization/vector_amplitude.py | 115 +++++++++++++++++- .../plot_inv_mag_MVI_VectorAmplitude.py | 8 +- 4 files changed, 144 insertions(+), 34 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 82296cd33c..9fa3a1b710 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -1561,6 +1561,8 @@ def update(self): Compute explicitly the main diagonal of JtJ """ + + jtj_diag = np.zeros_like(self.invProb.model) m = self.invProb.model @@ -1578,17 +1580,27 @@ def update(self): # Normalize and threshold weights wr = np.zeros_like(self.invProb.model) for reg in self.reg.objfcts: - if not isinstance(reg, BaseSimilarityMeasure): + if isinstance(reg, BaseSimilarityMeasure): + continue + + if hasattr(reg.mapping, "maps"): + for _, wire in reg.mapping.maps: + wr += wire.deriv(self.invProb.model).T * ( + (wire * jtj_diag) / reg.regularization_mesh.vol ** 2.0 + ) + else: wr += reg.mapping.deriv(self.invProb.model).T * ( - (reg.mapping * jtj_diag) / reg.regularization_mesh.vol ** 2.0 + (reg.mapping * jtj_diag) / reg.regularization_mesh.vol ** 2.0 ) + wr /= wr.max() wr += self.threshold wr **= 0.5 for reg in self.reg.objfcts: - if not isinstance(reg, BaseSimilarityMeasure): - for sub_reg in reg.objfcts: - sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) + if isinstance(reg, BaseSimilarityMeasure): + continue + for sub_reg in reg.objfcts: + sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) def validate(self, directiveList): # check if a beta estimator is in the list after setting the weights diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 8ce7622d1c..8b63d676e5 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -649,23 +649,18 @@ def nP(self): return self._nP -class Amplitude(Wires, IdentityMap): +class Group(Wires, IdentityMap): + def __init__(self, *args): + super(Group, self).__init__(*args) + self._maps = ((name, wire) for (name, wire) in self.maps if name != "_") + self._nP = int(np.sum([wire.shape[0] for (_, wire) in self.maps])) + def __mul__(self, val): assert isinstance(val, np.ndarray) - amplitude = np.linalg.norm(np.c_[[w * val for _, w in self.maps]], axis=0) - return amplitude - - def deriv(self, m, v=None): - """ - :param numpy.ndarray m: model - :rtype: scipy.sparse.csr_matrix - :return: derivative of transformed model - """ - deriv = 0 - for _, map in self.maps: - deriv += map.deriv(m, v) - - return deriv + split = [] + for n, w in self.maps: + split += [w * val] + return tuple(split) def deriv(self, m, v=None): """ @@ -673,11 +668,11 @@ def deriv(self, m, v=None): :rtype: scipy.sparse.csr_matrix :return: derivative of transformed model """ - deriv = 0 - for _, map in self.maps: - deriv += map.deriv(m, v) + deriv = [] + for name, wire in self.maps: + deriv += [wire.deriv(m, v)] - return deriv + return sp.vstack(deriv) class SelfConsistentEffectiveMedium(IdentityMap, properties.HasProperties): diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 3b6af7544d..9c0ae28fa5 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -26,12 +26,13 @@ class BaseVectorAmplitude(BaseRegularization): Base vector amplitude function. """ _projection = None + _W = None def __init__(self, mesh, **kwargs): super().__init__(mesh, **kwargs) @property - def mapping(self): + def mapping(self) -> maps.Wires: return self._mapping @mapping.setter @@ -48,12 +49,69 @@ def mapping(self, wires): def amplitude_map(self, m): """Create sparse vector model.""" - amplitude = 0 - for wire_model in (self.mapping * m): - amplitude += wire_model**2. + # amplitude = 0 + # for _, wire in self.mapping.maps: + # amplitude += (wire * m)**2. + + return np.linalg.norm(m, axis=0) + + def set_weights(self, **weights): + """Adds (or updates) the specified weights to the regularization + + Parameters: + ----------- + **kwargs : key, numpy.ndarray + Each keyword argument is added to the weights used by the regularization. + They can be accessed with their keyword argument. + + Examples + -------- + >>> import discretize + >>> from SimPEG.regularization import Small + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Small(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + """ + for key, values in weights.items(): + if not isinstance(values, tuple): + values = (values,) * len(self.mapping.maps) + + if len(values) != len(self.mapping.maps): + raise ValueError(f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})") + + self._weights[key] = {} + for (name, _), value in zip(self.mapping.maps, values): + self.validate_array_type("weights", value, float) + self.validate_shape("weights", value, self._weights_shapes) + self._weights[key][name] = value + + self._W = None + + @utils.timeIt + def __call__(self, m): + """ + """ + r = self.W * self.f_m(m) + return 0.5 * r.dot(r) - return amplitude**0.5 + @utils.timeIt + def deriv(self, m) -> np.ndarray: + """ + """ + r = self.W * self.f_m(m) + return self.f_m_deriv(m).T * (self.W.T * r) + + @utils.timeIt + def deriv2(self, m, v=None) -> csr_matrix: + """ + """ + f_m_deriv = self.f_m_deriv(m) + if v is None: + return f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + return f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): """ @@ -75,6 +133,23 @@ def f_m_deriv(self, m) -> csr_matrix: return self.mapping.deriv(self._delta_m(m)) + @property + def W(self): + """ + Weighting matrix + """ + if getattr(self, "_W", None) is None: + self._W = {name: 1.0 for name, _ in self.mapping.maps} + + for name, wire in self.mapping.maps: + + for weight in self._weights.values(): + self._W[name] *= weight[name] + + self._W[name] = utils.sdiag(self._W[name] ** 0.5) + + return self._W + class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): """ @@ -89,7 +164,35 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: m = self.amplitude_map(self.mapping * self._delta_m(m)) - return self.cell_gradient @ self.mapping.deriv(m) + + deriv = [] + for _, wire in self.mapping.maps: + deriv += [self.cell_gradient @ wire.deriv(m)] + return sp.vstack(deriv) + + @property + def W(self): + """ + Weighting matrix that takes the volumes, free weights, fixed weights and + length scales of the difference operator (normalized optional). + """ + if getattr(self, "_W", None) is None: + average_cell_2_face = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ) + self._W = {name: 1.0 for name, _ in self.mapping.maps} + + for name, wire in self.mapping.maps: + + for weight in self._weights.values(): + if weight[name].shape[0] == self.regularization_mesh.nC: + weight[name] = average_cell_2_face * weight[name] + + self._W[name] *= weight[name] + + self._W[name] = utils.sdiag(self._W[name] ** 0.5) + + return self._W class VectorAmplitude(Sparse): diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 92b1034561..f27c698e59 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -96,7 +96,7 @@ # # Create a mesh -h = [5, 5, 5] +h = [10, 10, 10] padDist = np.ones((3, 2)) * 100 mesh = mesh_builder_xyz( @@ -271,7 +271,7 @@ def plotVectorSectionsOctree( model, axs=ax, normal="Y", - ind=66, + # ind=66, actvMap=actv_plot, scale=0.5, vmin=0.0, @@ -303,14 +303,14 @@ def plotVectorSectionsOctree( # This Mapping connects the regularizations for the three-component # vector model -wires = maps.Amplitude(("p", nC), ("s", nC), ("t", nC)) +wires = maps.Group(("p", nC), ("s", nC), ("t", nC)) m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components # of magnetization -reg = regularization.Sparse(mesh, active_cells=actv, mapping=wires) +reg = regularization.VectorAmplitude(mesh, wires, active_cells=actv) reg.reference_model = np.zeros(3*nC) # Data misfit function From 048b898dc15eeefddf2ce249bc957acda7758ee9 Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 12 Sep 2022 10:18:04 -0700 Subject: [PATCH 007/455] Work on W --- SimPEG/regularization/vector_amplitude.py | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 9c0ae28fa5..c1ec0cf9d1 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -113,6 +113,7 @@ def deriv2(self, m, v=None) -> csr_matrix: return f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) + class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): """ Sparse smallness regularization on vector amplitude. @@ -139,15 +140,16 @@ def W(self): Weighting matrix """ if getattr(self, "_W", None) is None: - self._W = {name: 1.0 for name, _ in self.mapping.maps} - - for name, wire in self.mapping.maps: + self._W = [] + for name, _ in self.mapping.maps: + self._W.append(1.0) for weight in self._weights.values(): - self._W[name] *= weight[name] + self._W[-1] *= weight[name] - self._W[name] = utils.sdiag(self._W[name] ** 0.5) + self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) + self._W = sp.block_diag(self._W) return self._W @@ -180,17 +182,22 @@ def W(self): average_cell_2_face = getattr( self.regularization_mesh, "aveCC2F{}".format(self.orientation) ) - self._W = {name: 1.0 for name, _ in self.mapping.maps} + self._W = [] - for name, wire in self.mapping.maps: + for name, _ in self.mapping.maps: + + self._W.append(-1) for weight in self._weights.values(): - if weight[name].shape[0] == self.regularization_mesh.nC: - weight[name] = average_cell_2_face * weight[name] + values = weight[name] + if values.shape[0] == self.regularization_mesh.nC: + values = average_cell_2_face * values + + self._W[-1] *= values - self._W[name] *= weight[name] + self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) - self._W[name] = utils.sdiag(self._W[name] ** 0.5) + self._W = sp.block_diag(self._W) return self._W From 76297d7bfb7e279e3413599bcc31155cd5914db8 Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 12 Sep 2022 14:39:05 -0700 Subject: [PATCH 008/455] Loop over derivatives --- SimPEG/regularization/vector_amplitude.py | 34 ++++++++++++------- .../plot_inv_mag_MVI_VectorAmplitude.py | 4 +-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index c1ec0cf9d1..d0841f43d1 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -101,17 +101,23 @@ def deriv(self, m) -> np.ndarray: """ """ r = self.W * self.f_m(m) - return self.f_m_deriv(m).T * (self.W.T * r) + f_m_derivs = 0. + for f_m_deriv in self.f_m_deriv(m): + f_m_derivs += f_m_deriv.T * (self.W.T * r) + return f_m_derivs @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: """ """ - f_m_deriv = self.f_m_deriv(m) - if v is None: - return f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + f_m_derivs = 0. + for f_m_deriv in self.f_m_deriv(m): + if v is None: + f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + else: + f_m_derivs += f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) - return f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) + return f_m_derivs class VectorAmplitudeSmall(SparseSmall, BaseVectorAmplitude): @@ -132,7 +138,11 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: - return self.mapping.deriv(self._delta_m(m)) + deriv = [] + for _, wire in self.mapping.maps: + deriv.append(wire.deriv(m) * self.mapping.deriv(self._delta_m(m))) + + return deriv @property def W(self): @@ -149,7 +159,7 @@ def W(self): self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) - self._W = sp.block_diag(self._W) + self._W = sp.vstack(self._W) return self._W @@ -165,12 +175,12 @@ def f_m(self, m): return dfm_dl def f_m_deriv(self, m) -> csr_matrix: - m = self.amplitude_map(self.mapping * self._delta_m(m)) deriv = [] for _, wire in self.mapping.maps: - deriv += [self.cell_gradient @ wire.deriv(m)] - return sp.vstack(deriv) + deriv.append(self.cell_gradient * wire.deriv(m) * self.mapping.deriv(self._delta_m(m))) + + return deriv @property def W(self): @@ -186,7 +196,7 @@ def W(self): for name, _ in self.mapping.maps: - self._W.append(-1) + self._W.append(1.0) for weight in self._weights.values(): values = weight[name] @@ -197,7 +207,7 @@ def W(self): self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) - self._W = sp.block_diag(self._W) + self._W = sp.vstack(self._W) return self._W diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index f27c698e59..1451d1d98e 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -96,7 +96,7 @@ # # Create a mesh -h = [10, 10, 10] +h = [5, 5, 5] padDist = np.ones((3, 2)) * 100 mesh = mesh_builder_xyz( @@ -271,7 +271,7 @@ def plotVectorSectionsOctree( model, axs=ax, normal="Y", - # ind=66, + ind=66, actvMap=actv_plot, scale=0.5, vmin=0.0, From a240297a713fac91e8b8e200d5502b8abf60f4c0 Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 12 Sep 2022 15:32:30 -0700 Subject: [PATCH 009/455] Fist good run with L2 --- SimPEG/regularization/sparse.py | 10 ++++++---- SimPEG/regularization/vector_amplitude.py | 4 ++-- .../03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 803c957ba8..c91eed99ec 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -136,10 +136,12 @@ def update_weights(self, m): Compute and store the irls weights. """ if self.gradient_type == "total": - if self.reference_model_in_smooth: - delta_m = self.mapping * self._delta_m(m) - else: - delta_m = self.mapping * m + # if self.reference_model_in_smooth: + # delta_m = self.mapping * self._delta_m(m) + # else: + # delta_m = self.mapping * m + + delta_m = self.f_m(m) f_m = np.zeros_like(delta_m) for ii, comp in enumerate("xyz"): diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index d0841f43d1..cb0994b61a 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -100,10 +100,10 @@ def __call__(self, m): def deriv(self, m) -> np.ndarray: """ """ - r = self.W * self.f_m(m) + # r = self.W * self.f_m(m) f_m_derivs = 0. for f_m_deriv in self.f_m_deriv(m): - f_m_derivs += f_m_deriv.T * (self.W.T * r) + f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * m) return f_m_derivs @utils.timeIt diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 1451d1d98e..4d1cfdcb32 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -333,7 +333,7 @@ def plotVectorSectionsOctree( # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) +IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() From a64fabdc1ab09cd9be2cd88064e3f3a2b07a15ee Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 12 Sep 2022 16:21:14 -0700 Subject: [PATCH 010/455] First full IRLS run!! --- SimPEG/maps.py | 4 ++-- SimPEG/regularization/sparse.py | 10 ++++------ SimPEG/regularization/vector_amplitude.py | 14 +++++--------- .../plot_inv_mag_MVI_VectorAmplitude.py | 14 ++++++++------ 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 8b63d676e5..33e90e6956 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -660,7 +660,7 @@ def __mul__(self, val): split = [] for n, w in self.maps: split += [w * val] - return tuple(split) + return np.linalg.norm(split, axis=0) def deriv(self, m, v=None): """ @@ -672,7 +672,7 @@ def deriv(self, m, v=None): for name, wire in self.maps: deriv += [wire.deriv(m, v)] - return sp.vstack(deriv) + return deriv class SelfConsistentEffectiveMedium(IdentityMap, properties.HasProperties): diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index c91eed99ec..803c957ba8 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -136,12 +136,10 @@ def update_weights(self, m): Compute and store the irls weights. """ if self.gradient_type == "total": - # if self.reference_model_in_smooth: - # delta_m = self.mapping * self._delta_m(m) - # else: - # delta_m = self.mapping * m - - delta_m = self.f_m(m) + if self.reference_model_in_smooth: + delta_m = self.mapping * self._delta_m(m) + else: + delta_m = self.mapping * m f_m = np.zeros_like(delta_m) for ii, comp in enumerate("xyz"): diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index cb0994b61a..53bd042d87 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -134,15 +134,11 @@ def f_m(self, m): Compute the amplitude of a vector model. """ - return self.amplitude_map(self.mapping * self._delta_m(m)) + return self.mapping * self._delta_m(m) def f_m_deriv(self, m) -> csr_matrix: - deriv = [] - for _, wire in self.mapping.maps: - deriv.append(wire.deriv(m) * self.mapping.deriv(self._delta_m(m))) - - return deriv + return self.mapping.deriv(self._delta_m(m)) @property def W(self): @@ -169,7 +165,7 @@ class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): """ def f_m(self, m): - m = self.amplitude_map(self.mapping * self._delta_m(m)) + m = self.mapping * self._delta_m(m) dfm_dl = self.cell_gradient @ m return dfm_dl @@ -177,8 +173,8 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] - for _, wire in self.mapping.maps: - deriv.append(self.cell_gradient * wire.deriv(m) * self.mapping.deriv(self._delta_m(m))) + for map_deriv in self.mapping.deriv(self._delta_m(m)): + deriv.append(self.cell_gradient * map_deriv) return deriv diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 4d1cfdcb32..f310c07ce1 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -312,6 +312,8 @@ def plotVectorSectionsOctree( # of magnetization reg = regularization.VectorAmplitude(mesh, wires, active_cells=actv) reg.reference_model = np.zeros(3*nC) +reg.norms = [0, 0, 0, 0] +reg.gradient_type = "components" # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -319,7 +321,7 @@ def plotVectorSectionsOctree( # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 + maxIter=20, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -333,7 +335,7 @@ def plotVectorSectionsOctree( # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1) +IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() @@ -356,10 +358,10 @@ def plotVectorSectionsOctree( # plt.figure(figsize=(8, 8)) -ax = plt.subplot(2, 1, 1) +ax = plt.subplot(2, 1, 2) plotVectorSectionsOctree( mesh, - mrec_MVIC.reshape((nC, 3), order="F"), + invProb.l2model.reshape((nC, 3), order="F"), axs=ax, normal="Y", ind=65, @@ -379,7 +381,7 @@ def plotVectorSectionsOctree( plotVectorSectionsOctree( mesh, - invProb.l2model.reshape((nC, 3), order="F"), + mrec_MVIC.reshape((nC, 3), order="F"), axs=ax, normal="Y", ind=65, @@ -390,7 +392,7 @@ def plotVectorSectionsOctree( ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) -ax.set_title("Sparse model (Spherical)") +ax.set_title("Sparse model (L0L2)") ax.set_xlabel("x") ax.set_ylabel("y") plt.gca().set_aspect("equal", adjustable="box") From 8a636943902052470b7a9b2c07c011e42cbe0575 Mon Sep 17 00:00:00 2001 From: fourndo Date: Tue, 13 Sep 2022 09:34:10 -0700 Subject: [PATCH 011/455] Move amplitude calc to reg --- SimPEG/maps.py | 9 ++------- SimPEG/regularization/vector_amplitude.py | 15 +++------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 33e90e6956..e7e42760c3 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -652,16 +652,11 @@ def nP(self): class Group(Wires, IdentityMap): def __init__(self, *args): super(Group, self).__init__(*args) + + # Remove wires that are not part of the group self._maps = ((name, wire) for (name, wire) in self.maps if name != "_") self._nP = int(np.sum([wire.shape[0] for (_, wire) in self.maps])) - def __mul__(self, val): - assert isinstance(val, np.ndarray) - split = [] - for n, w in self.maps: - split += [w * val] - return np.linalg.norm(split, axis=0) - def deriv(self, m, v=None): """ :param numpy.ndarray m: model diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 53bd042d87..c665cce510 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -47,14 +47,6 @@ def mapping(self, wires): ) self._mapping = wires - def amplitude_map(self, m): - """Create sparse vector model.""" - # amplitude = 0 - # for _, wire in self.mapping.maps: - # amplitude += (wire * m)**2. - - return np.linalg.norm(m, axis=0) - def set_weights(self, **weights): """Adds (or updates) the specified weights to the regularization @@ -134,7 +126,7 @@ def f_m(self, m): Compute the amplitude of a vector model. """ - return self.mapping * self._delta_m(m) + return np.linalg.norm(self.mapping * self._delta_m(m), axis=0) def f_m_deriv(self, m) -> csr_matrix: @@ -165,10 +157,9 @@ class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): """ def f_m(self, m): - m = self.mapping * self._delta_m(m) - dfm_dl = self.cell_gradient @ m + a = np.linalg.norm(self.mapping * self._delta_m(m), axis=0) - return dfm_dl + return self.cell_gradient @ a def f_m_deriv(self, m) -> csr_matrix: From 75377ace90346b5d2b23a35adc25da0872a0cb85 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 14 Sep 2022 10:05:37 -0700 Subject: [PATCH 012/455] Update class names for SmoothnessFirstOrder --- SimPEG/directives/directives.py | 4 +- SimPEG/directives/pgi_directives.py | 6 +-- SimPEG/regularization/__init__.py | 2 +- SimPEG/regularization/sparse.py | 8 ++-- SimPEG/regularization/vector_amplitude.py | 43 +++++++++---------- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 2 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 27 +++++++++--- 7 files changed, 53 insertions(+), 39 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index e36f699e00..17b38f0a67 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -17,7 +17,7 @@ PGIsmallness, PGIwithNonlinearRelationshipsSmallness, SmoothnessFirstOrder, - SparseDeriv, + SparseSmoothnessFirstOrder, BaseSimilarityMeasure, ) from ..utils import ( @@ -321,7 +321,7 @@ def initialize(self): ): smallness += [obj] - elif isinstance(obj, (SmoothnessFirstOrder, SparseDeriv)): + elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)): parents[obj] = regobjcts smoothness += [obj] diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index c7c2083519..87f28c3ea3 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -18,7 +18,7 @@ PGIwithNonlinearRelationshipsSmallness, PGI, SmoothnessFirstOrder, - SparseDeriv, + SparseSmoothnessFirstOrder, PGIwithRelationships, ) from ..utils import ( @@ -364,7 +364,7 @@ def initialize(self): [ i, j, - isinstance(regpart, (SmoothnessFirstOrder, SparseDeriv)), + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)), ] ] self.Smooth = np.r_[Smooth] @@ -381,7 +381,7 @@ def initialize(self): self.nbr = len(self.reg.objfcts) self.Smooth = np.r_[ [ - isinstance(regpart, (SmoothnessFirstOrder, SparseDeriv)) + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)) for regpart in self.reg.objfcts ] ] diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index d98b1d82e3..acce2845e5 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -8,7 +8,7 @@ SmoothnessSecondOrder, ) from .regularization_mesh import RegularizationMesh -from .sparse import BaseSparse, SparseSmallness, SparseDeriv, Sparse +from .sparse import BaseSparse, SparseSmallness, SparseSmoothnessFirstOrder, Sparse from .pgi import PGIsmallness, PGI from .cross_gradient import CrossGradient from .correspondence import LinearCorrespondence diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index c3c34619a6..ab092bc4c7 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -119,7 +119,7 @@ def update_weights(self, m): self.set_weights(irls=self.get_lp_weights(f_m)) -class SparseDeriv(BaseSparse, SmoothnessFirstOrder): +class SparseSmoothnessFirstOrder(BaseSparse, SmoothnessFirstOrder): """ Base Class for sparse regularization on first spatial derivatives """ @@ -243,14 +243,14 @@ def __init__( if objfcts is None: objfcts = [ SparseSmallness(mesh=self.regularization_mesh), - SparseDeriv(mesh=self.regularization_mesh, orientation="x"), + SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="x"), ] if mesh.dim > 1: - objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="y")) + objfcts.append(SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="y")) if mesh.dim > 2: - objfcts.append(SparseDeriv(mesh=self.regularization_mesh, orientation="z")) + objfcts.append(SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="z")) gradientType = kwargs.pop("gradientType", None) super().__init__( diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index ddd2a6bd82..3d14e36fe6 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -13,19 +13,19 @@ from .sparse import ( Sparse, SparseSmallness, - SparseDeriv + SparseSmoothnessFirstOrder ) from .. import utils +from SimPEG.utils.code_utils import validate_array_type, validate_shape if TYPE_CHECKING: from scipy.sparse import csr_matrix -class BaseVectorAmplitude(BaseRegularization): +class BaseAmplitude(BaseRegularization): """ Base vector amplitude function. """ - _projection = None _W = None def __init__(self, mesh, **kwargs): @@ -75,8 +75,8 @@ def set_weights(self, **weights): self._weights[key] = {} for (name, _), value in zip(self.mapping.maps, values): - self.validate_array_type("weights", value, float) - self.validate_shape("weights", value, self._weights_shapes) + validate_array_type("weights", value, float) + validate_shape("weights", value, self._weights_shapes) self._weights[key][name] = value self._W = None @@ -92,7 +92,6 @@ def __call__(self, m): def deriv(self, m) -> np.ndarray: """ """ - # r = self.W * self.f_m(m) f_m_derivs = 0. for f_m_deriv in self.f_m_deriv(m): f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * m) @@ -112,7 +111,7 @@ def deriv2(self, m, v=None) -> csr_matrix: return f_m_derivs -class VectorAmplitudeSmall(SparseSmallness, BaseVectorAmplitude): +class AmplitudeSmallness(SparseSmallness, BaseAmplitude): """ Sparse smallness regularization on vector amplitude. @@ -138,20 +137,20 @@ def W(self): Weighting matrix """ if getattr(self, "_W", None) is None: - self._W = [] + weights = [] for name, _ in self.mapping.maps: - self._W.append(1.0) + weights.append(1.0) for weight in self._weights.values(): - self._W[-1] *= weight[name] + weights[-1] *= weight[name] - self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) + weights[-1] = utils.sdiag(weights[-1] ** 0.5) - self._W = sp.vstack(self._W) + self._W = sp.vstack(weights) return self._W -class VectorAmplitudeDeriv(SparseDeriv, BaseVectorAmplitude): +class AmplitudeSmoothnessFirstOrder(SparseSmoothnessFirstOrder, BaseAmplitude): """ Base Class for sparse regularization on first spatial derivatives """ @@ -179,22 +178,22 @@ def W(self): average_cell_2_face = getattr( self.regularization_mesh, "aveCC2F{}".format(self.orientation) ) - self._W = [] + weights = [] for name, _ in self.mapping.maps: - self._W.append(1.0) + weights.append(1.0) for weight in self._weights.values(): values = weight[name] if values.shape[0] == self.regularization_mesh.nC: values = average_cell_2_face * values - self._W[-1] *= values + weights[-1] *= values - self._W[-1] = utils.sdiag(self._W[-1] ** 0.5) + weights[-1] = utils.sdiag(weights[-1] ** 0.5) - self._W = sp.vstack(self._W) + self._W = sp.vstack(weights) return self._W @@ -226,15 +225,15 @@ def __init__( self._regularization_mesh.active_cells = active_cells objfcts = [ - VectorAmplitudeSmall(mesh=self.regularization_mesh, mapping=wire_map), - VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="x"), + AmplitudeSmallness(mesh=self.regularization_mesh, mapping=wire_map), + AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="x"), ] if mesh.dim > 1: - objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="y")) + objfcts.append(AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="y")) if mesh.dim > 2: - objfcts.append(VectorAmplitudeDeriv(mesh=self.regularization_mesh, mapping=wire_map, orientation="z")) + objfcts.append(AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="z")) super().__init__( self.regularization_mesh, diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index d2d2038b10..c712b64e29 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -50,7 +50,7 @@ # As a simple case, we pick a vertical inducing field of magnitude 50,000 nT. # # -sp.random.seed(1) +np.random.seed(1) # We will assume a vertical inducing field H0 = (50000.0, 90.0, 0.0) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index f310c07ce1..369295f849 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -103,7 +103,7 @@ xyzLoc, h, padding_distance=padDist, depth_core=100, mesh_type="tree" ) mesh = refine_tree_xyz( - mesh, topo, method="surface", octree_levels=[4, 4], finalize=True + mesh, topo, method="surface", octree_levels=[2, 6], finalize=True ) @@ -188,7 +188,7 @@ def plotVectorSectionsOctree( axs = plt.subplot(111) if fill: - temp_mesh.plotImage(amp, ax=axs, clim=[vmin, vmax], grid=True) + temp_mesh.plotImage(amp, ax=axs, clim=[vmin, vmax], grid=False) axs.quiver( temp_mesh.gridCC[:, 0], @@ -246,7 +246,7 @@ def plotVectorSectionsOctree( # Compute some data and add some random noise d = simulation.dpred(mkvc(model)) -std = 5 # nT +std = 10 # nT synthetic_data = d + np.random.randn(len(d)) * std wd = np.ones(len(d)) * std @@ -315,6 +315,21 @@ def plotVectorSectionsOctree( reg.norms = [0, 0, 0, 0] reg.gradient_type = "components" + +# Create three regularizations for the different components +# of magnetization +# reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.p) +# reg_p.reference_model = np.zeros(3 * nC) +# +# reg_s = regularization.Sparse(mesh, active_cells=actv, mapping=wires.s) +# reg_s.reference_model = np.zeros(3 * nC) +# +# reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.t) +# reg_t.reference_model = np.zeros(3 * nC) +# +# reg = reg_p + reg_s + reg_t +# reg.reference_model = np.zeros(3 * nC) + # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) dmis.W = 1.0 / data_object.standard_deviation @@ -335,7 +350,7 @@ def plotVectorSectionsOctree( # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) +IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=2, beta_tol=5e-1) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() @@ -358,7 +373,7 @@ def plotVectorSectionsOctree( # plt.figure(figsize=(8, 8)) -ax = plt.subplot(2, 1, 2) +ax = plt.subplot(2, 1, 1) plotVectorSectionsOctree( mesh, invProb.l2model.reshape((nC, 3), order="F"), @@ -386,7 +401,7 @@ def plotVectorSectionsOctree( normal="Y", ind=65, actvMap=actv_plot, - scale=0.4, + scale=2.0, vmin=0.0, vmax=0.025, ) From 98ac53182d8ab7b862805c7ad2a90f42dec24e15 Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 15 Sep 2022 12:48:39 -0700 Subject: [PATCH 013/455] Add new directive to update ref. Great run with reference angle. --- SimPEG/directives/__init__.py | 1 + SimPEG/directives/directives.py | 45 +++- .../plot_inv_mag_MVI_VectorAmplitude.py | 210 +++++++++++------- 3 files changed, 173 insertions(+), 83 deletions(-) diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py index aed37d2e10..3ef349dd73 100644 --- a/SimPEG/directives/__init__.py +++ b/SimPEG/directives/__init__.py @@ -17,6 +17,7 @@ JointScalingSchedule, UpdateSensitivityWeights, ProjectSphericalBounds, + UpdateReferenceVector ) from .pgi_directives import ( diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 17b38f0a67..3daec4cf31 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -31,6 +31,7 @@ eigenvalue_by_power_iteration, ) from ..utils.code_utils import deprecate_property +from ..utils.mat_utils import dip_azimuth2cartesian from .. import optimization @@ -1229,12 +1230,17 @@ def initialize(self): self.norms = [] for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue self.norms.append(reg.norms) reg.norms = [2.0 for obj in reg.objfcts] reg.model = self.invProb.model # Update the model used by the regularization for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + reg.model = self.invProb.model if self.sphericalDomain: @@ -1288,6 +1294,9 @@ def endIter(self): # Print to screen for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + for obj in reg.objfcts: if isinstance(reg, (Sparse, BaseSparse)): obj.irls_threshold = obj.irls_threshold / self.coolEpsFact @@ -1297,6 +1306,8 @@ def endIter(self): # Reset the regularization matrices so that it is # recalculated for current model. Do it to all levels of comboObj for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue if isinstance(reg, (Sparse, BaseSparse)): reg.update_weights(reg.model) @@ -1339,6 +1350,8 @@ def start_irls(self): # Re-assign the norms supplied by user l2 -> lp for reg, norms in zip(self.reg.objfcts, self.norms): + if not isinstance(reg, Sparse): + continue reg.norms = norms # Save l2-model @@ -1346,6 +1359,8 @@ def start_irls(self): # Print to screen for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue if not self.silent: print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) @@ -1599,8 +1614,8 @@ def update(self): for reg in self.reg.objfcts: if isinstance(reg, BaseSimilarityMeasure): continue - for sub_reg in reg.objfcts: - sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) + + reg.set_weights(sensitivity=reg.mapping * wr) def validate(self, directiveList): # check if a beta estimator is in the list after setting the weights @@ -1626,6 +1641,32 @@ def validate(self, directiveList): return True +class UpdateReferenceVector(InversionDirective): + def __init__(self, regularization, azimuth, dip): + self._regularization = regularization + self._reference = mkvc(dip_azimuth2cartesian(azimuth, dip)) + + def initialize(self): + reference_vector = self.get_normalized_reference() + for objfct in self._regularization.objfcts: + objfct.reference_model = reference_vector + + def endIter(self): + reference_vector = self.get_normalized_reference() + for objfct in self._regularization.objfcts: + objfct.reference_model = reference_vector + + def get_normalized_reference(self): + n_comp = len(self._regularization.objfcts) + model = [] + for objfct in self._regularization.objfcts: + model.append(objfct.mapping * self.invProb.model) + + amplitude = np.linalg.norm(np.vstack(model).T, axis=1) + return sdiag( + np.kron(np.ones(n_comp), amplitude) + ) * self._reference + class ProjectSphericalBounds(InversionDirective): """ Trick for spherical coordinate system. diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 369295f849..eacfdcfe63 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -23,7 +23,7 @@ ) from SimPEG import utils -from SimPEG.utils import mkvc +from SimPEG.utils import mkvc, sdiag from discretize.utils import mesh_builder_xyz, refine_tree_xyz from SimPEG.potential_fields import magnetics @@ -49,7 +49,10 @@ H0 = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) -M = np.array([45.0, 90.0]) +M = [ + np.c_[45.0, 270.0], + np.c_[45.0, 90.0], +] # Create grid of points for topography # Lets create a simple Gaussian topo and set the active cells @@ -74,13 +77,13 @@ # Here how the topography looks with a quick interpolation, just a Gaussian... tri = sp.spatial.Delaunay(topo) -fig = plt.figure() -ax = fig.add_subplot(1, 1, 1, projection="3d") -ax.plot_trisurf( - topo[:, 0], topo[:, 1], topo[:, 2], triangles=tri.simplices, cmap=plt.cm.Spectral -) -ax.scatter3D(xyzLoc[:, 0], xyzLoc[:, 1], xyzLoc[:, 2], c="k") -plt.show() +# fig = plt.figure() +# ax = fig.add_subplot(1, 1, 1, projection="3d") +# ax.plot_trisurf( +# topo[:, 0], topo[:, 1], topo[:, 2], triangles=tri.simplices, cmap=plt.cm.Spectral +# ) +# ax.scatter3D(xyzLoc[:, 0], xyzLoc[:, 1], xyzLoc[:, 2], c="k") +# plt.show() ############################################################################### # Inversion Mesh @@ -183,7 +186,7 @@ def plotVectorSectionsOctree( ind_3d_to_2d = mesh._get_containing_cell_indexes(tm_gridboost) v2d = m[ind_3d_to_2d, :] amp = np.sum(v2d ** 2.0, axis=1) ** 0.5 - + v2d = sdiag(1./amp) * v2d if axs is None: axs = plt.subplot(111) @@ -215,23 +218,33 @@ def plotVectorSectionsOctree( # -model = np.zeros((mesh.nC, 3)) +model_azm_dip = np.zeros((mesh.nC, 2)) +model_amp = np.zeros(mesh.nC) +for ii, anomaly in enumerate(M): + x_shift = 120.0**ii + # Get the indicies of the magnetized block + ind = utils.model_builder.getIndicesBlock( + np.r_[-80 + x_shift, -20, -10], + np.r_[-40 + x_shift, 20, 25], + mesh.gridCC, + )[0] -# Convert the inclination declination to vector in Cartesian -M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) + # Assign magnetization values + # model_azm_dip[ind, :] = np.kron(np.ones((ind.shape[0], 1)), anomaly) + model_amp[ind] = 0.05 -# Get the indicies of the magnetized block -ind = utils.model_builder.getIndicesBlock( - np.r_[-20, -20, -10], - np.r_[20, 20, 25], - mesh.gridCC, -)[0] - -# Assign magnetization values -model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) +model_azm_dip[mesh.cell_centers[:, 0] < 0, 0] = M[0][0, 0] +model_azm_dip[mesh.cell_centers[:, 0] < 0, 1] = M[0][0, 1] +model_azm_dip[mesh.cell_centers[:, 0] >= 0, 0] = M[1][0, 0] +model_azm_dip[mesh.cell_centers[:, 0] >= 0, 1] = M[1][0, 1] # Remove air cells -model = model[actv, :] +model_azm_dip = model_azm_dip[actv, :] +model_amp = model_amp[actv] + +model = sdiag(model_amp) * utils.mat_utils.dip_azimuth2cartesian( + model_azm_dip[:, 0], model_azm_dip[:, 1] +) # Create active map to go from reduce set to full actvMap = maps.InjectActiveCells(mesh, actv, np.nan) @@ -257,33 +270,33 @@ def plotVectorSectionsOctree( actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) # Plot the model and data -plt.figure() -ax = plt.subplot(2, 1, 1) -im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) -plt.colorbar(im[0]) -ax.set_title("Predicted data.") -plt.gca().set_aspect("equal", adjustable="box") - -# Plot the vector model -ax = plt.subplot(2, 1, 2) -plotVectorSectionsOctree( - mesh, - model, - axs=ax, - normal="Y", - ind=66, - actvMap=actv_plot, - scale=0.5, - vmin=0.0, - vmax=0.025, -) -ax.set_xlim([-200, 200]) -ax.set_ylim([-100, 75]) -ax.set_xlabel("x") -ax.set_ylabel("y") -plt.gca().set_aspect("equal", adjustable="box") - -plt.show() +# plt.figure() +# ax = plt.subplot(2, 1, 1) +# im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) +# plt.colorbar(im[0]) +# ax.set_title("Predicted data.") +# plt.gca().set_aspect("equal", adjustable="box") +# +# # Plot the vector model +# ax = plt.subplot(2, 1, 2) +# plotVectorSectionsOctree( +# mesh, +# model, +# axs=ax, +# normal="Y", +# ind=66, +# actvMap=actv_plot, +# scale=10, +# vmin=0.0, +# vmax=0.025, +# ) +# ax.set_xlim([-200, 200]) +# ax.set_ylim([-100, 75]) +# ax.set_xlabel("x") +# ax.set_ylabel("y") +# plt.gca().set_aspect("equal", adjustable="box") +# +# plt.show() ###################################################################### @@ -310,25 +323,49 @@ def plotVectorSectionsOctree( # Create three regularizations for the different components # of magnetization -reg = regularization.VectorAmplitude(mesh, wires, active_cells=actv) -reg.reference_model = np.zeros(3*nC) -reg.norms = [0, 0, 0, 0] -reg.gradient_type = "components" - +reg_amp = regularization.VectorAmplitude(mesh, wires, active_cells=actv) +reg_amp.reference_model = np.zeros(3*nC) +reg_amp.norms = [2, 2, 2, 2] +reg_amp.gradient_type = "components" # Create three regularizations for the different components # of magnetization -# reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.p) -# reg_p.reference_model = np.zeros(3 * nC) -# -# reg_s = regularization.Sparse(mesh, active_cells=actv, mapping=wires.s) -# reg_s.reference_model = np.zeros(3 * nC) -# -# reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.t) -# reg_t.reference_model = np.zeros(3 * nC) -# -# reg = reg_p + reg_s + reg_t -# reg.reference_model = np.zeros(3 * nC) +reg_p = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.p, + reference_model_in_smooth=True, + # alpha_s=0, + # alpha_x=0, + # alpha_y=0, + # alpha_z=0, + norms=[0, 0, 0, 0] +) +reg_s = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.s, + reference_model_in_smooth=True, + # alpha_s=0, + # alpha_x=0, + # alpha_y=0, + # alpha_z=0, + norms=[0, 0, 0, 0] +) +reg_t = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.t, + reference_model_in_smooth=True, + # alpha_s=0, + # alpha_x=0, + # alpha_y=0, + # alpha_z=0, + norms=[0, 0, 0, 0] +) +reg_components = reg_p + reg_s + reg_t + +reg = reg_amp + reg_components # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -350,13 +387,24 @@ def plotVectorSectionsOctree( # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=2, beta_tol=5e-1) +IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() +# Update reference model +update_ref = directives.UpdateReferenceVector( + reg_components, model_azm_dip[:, 0], model_azm_dip[:, 1] +) + inv = inversion.BaseInversion( - invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] + invProb, directiveList=[ + sensitivity_weights, + update_ref, + IRLS, + update_Jacobi, + betaest + ] ) # Run the inversion @@ -381,9 +429,9 @@ def plotVectorSectionsOctree( normal="Y", ind=65, actvMap=actv_plot, - scale=0.05, + scale=10, vmin=0.0, - vmax=0.005, + vmax=0.01, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -401,7 +449,7 @@ def plotVectorSectionsOctree( normal="Y", ind=65, actvMap=actv_plot, - scale=2.0, + scale=10.0, vmin=0.0, vmax=0.025, ) @@ -415,13 +463,13 @@ def plotVectorSectionsOctree( plt.show() # Plot the final predicted data and the residual -plt.figure() -ax = plt.subplot(1, 2, 1) -utils.plot_utils.plot2Ddata(xyzLoc, invProb.dpred, ax=ax) -ax.set_title("Predicted data.") -plt.gca().set_aspect("equal", adjustable="box") - -ax = plt.subplot(1, 2, 2) -utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data - invProb.dpred, ax=ax) -ax.set_title("Data residual.") -plt.gca().set_aspect("equal", adjustable="box") +# plt.figure() +# ax = plt.subplot(1, 2, 1) +# utils.plot_utils.plot2Ddata(xyzLoc, invProb.dpred, ax=ax) +# ax.set_title("Predicted data.") +# plt.gca().set_aspect("equal", adjustable="box") +# +# ax = plt.subplot(1, 2, 2) +# utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data - invProb.dpred, ax=ax) +# ax.set_title("Data residual.") +# plt.gca().set_aspect("equal", adjustable="box") From 83239009d88634fa4a64027bea18425f42d74ce4 Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 19 Sep 2022 09:32:33 -0700 Subject: [PATCH 014/455] Formating of example --- .../plot_inv_mag_MVI_VectorAmplitude.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index eacfdcfe63..5b3c785207 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -325,7 +325,7 @@ def plotVectorSectionsOctree( # of magnetization reg_amp = regularization.VectorAmplitude(mesh, wires, active_cells=actv) reg_amp.reference_model = np.zeros(3*nC) -reg_amp.norms = [2, 2, 2, 2] +reg_amp.norms = [0, 0, 0, 0] reg_amp.gradient_type = "components" # Create three regularizations for the different components @@ -335,10 +335,6 @@ def plotVectorSectionsOctree( active_cells=actv, mapping=wires.p, reference_model_in_smooth=True, - # alpha_s=0, - # alpha_x=0, - # alpha_y=0, - # alpha_z=0, norms=[0, 0, 0, 0] ) reg_s = regularization.Sparse( @@ -346,10 +342,6 @@ def plotVectorSectionsOctree( active_cells=actv, mapping=wires.s, reference_model_in_smooth=True, - # alpha_s=0, - # alpha_x=0, - # alpha_y=0, - # alpha_z=0, norms=[0, 0, 0, 0] ) reg_t = regularization.Sparse( @@ -357,15 +349,11 @@ def plotVectorSectionsOctree( active_cells=actv, mapping=wires.t, reference_model_in_smooth=True, - # alpha_s=0, - # alpha_x=0, - # alpha_y=0, - # alpha_z=0, norms=[0, 0, 0, 0] ) reg_components = reg_p + reg_s + reg_t -reg = reg_amp + reg_components +reg = reg_components # + reg_amp# + reg_components # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -431,7 +419,7 @@ def plotVectorSectionsOctree( actvMap=actv_plot, scale=10, vmin=0.0, - vmax=0.01, + # vmax=0.01, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -451,7 +439,7 @@ def plotVectorSectionsOctree( actvMap=actv_plot, scale=10.0, vmin=0.0, - vmax=0.025, + # vmax=0.025, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) From 384aaba0f4f28cfe60a80849d248a5d6c152f207 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Oct 2022 15:27:17 -0700 Subject: [PATCH 015/455] Default to row --- SimPEG/dask/potential_fields/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/dask/potential_fields/base.py b/SimPEG/dask/potential_fields/base.py index cb75a03c3e..d17a604c7c 100644 --- a/SimPEG/dask/potential_fields/base.py +++ b/SimPEG/dask/potential_fields/base.py @@ -5,7 +5,7 @@ from dask.diagnostics import ProgressBar from ..utils import compute_chunk_sizes -Sim._chunk_format = "equal" +Sim._chunk_format = "row" @property From d4d6582190f1a2dfda1302dce0f7b640164d245c Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 3 Oct 2022 09:00:00 -0700 Subject: [PATCH 016/455] Fix total sparse --- SimPEG/maps.py | 5 ++- SimPEG/regularization/sparse.py | 22 +++++------- SimPEG/regularization/vector_amplitude.py | 35 +++++++++++++++++++ .../plot_inv_mag_MVI_VectorAmplitude.py | 5 +-- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index e7e42760c3..9ad15f4f59 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -1587,7 +1587,10 @@ def nP(self): return int(self.indActive.sum()) def _transform(self, m): - return self.P * m + self.valInactive + if m.ndim > 1: + return self.P * m + self.valInactive[:, None] + else: + return self.P * m + self.valInactive def inverse(self, D): return self.P.T * D diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index ab092bc4c7..fd9d462bad 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -136,31 +136,25 @@ def update_weights(self, m): Compute and store the irls weights. """ if self.gradient_type == "total": - if self.reference_model_in_smooth: - delta_m = self.mapping * self._delta_m(m) - else: - delta_m = self.mapping * m - + delta_m = self.mapping * self._delta_m(m) f_m = np.zeros_like(delta_m) for ii, comp in enumerate("xyz"): if self.regularization_mesh.dim > ii: - Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") - length_scales = Ave * ( - self.regularization_mesh.Pac.T - * self.regularization_mesh.mesh.h_gridded[:, ii] - ) dm = ( getattr(self.regularization_mesh, f"cell_gradient_{comp}") * delta_m ) if self.units is not None and self.units.lower() == "radian": - dm = utils.mat_utils.coterminal(dm) - - dm_dl = dm / length_scales + Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") + length_scales = Ave * ( + self.regularization_mesh.Pac.T + * self.regularization_mesh.mesh.h_gridded[:, ii] + ) + dm = utils.mat_utils.coterminal(dm * length_scales) / length_scales f_m += np.abs( - getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm_dl + getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm ) f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 3d14e36fe6..9bb98f5f62 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -197,6 +197,41 @@ def W(self): return self._W + def update_weights(self, m): + """ + Compute and store the irls weights. + """ + if self.gradient_type == "total": + delta_m = self.mapping * self._delta_m(m) + delta_m = np.linalg.norm(delta_m, axis=0) + f_m = np.zeros_like(delta_m) + + for ii, comp in enumerate("xyz"): + if self.regularization_mesh.dim > ii: + dm = ( + getattr(self.regularization_mesh, f"cell_gradient_{comp}") + * delta_m + ) + + if self.units is not None and self.units.lower() == "radian": + Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") + length_scales = Ave * ( + self.regularization_mesh.Pac.T + * self.regularization_mesh.mesh.h_gridded[:, ii] + ) + dm = utils.mat_utils.coterminal(dm * length_scales) / length_scales + + f_m += np.abs( + getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm + ) + + f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m + + else: + f_m = self.f_m(m) + + self.set_weights(irls=self.get_lp_weights(f_m)) + class VectorAmplitude(Sparse): """ diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 5b3c785207..828f6d0844 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -325,7 +325,8 @@ def plotVectorSectionsOctree( # of magnetization reg_amp = regularization.VectorAmplitude(mesh, wires, active_cells=actv) reg_amp.reference_model = np.zeros(3*nC) -reg_amp.norms = [0, 0, 0, 0] +reg_amp.norms = [2, 0, 0, 0] +# reg_amp.alpha_s = 0.0 reg_amp.gradient_type = "components" # Create three regularizations for the different components @@ -353,7 +354,7 @@ def plotVectorSectionsOctree( ) reg_components = reg_p + reg_s + reg_t -reg = reg_components # + reg_amp# + reg_components +reg = reg_amp# + reg_components # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) From 84c4ea9efa45c867521b9bda97baffe0d1c183fe Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Oct 2022 08:45:26 -0700 Subject: [PATCH 017/455] generalize directive reference vector for amplitude or direction --- SimPEG/directives/directives.py | 57 ++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 3daec4cf31..22092af6aa 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -31,7 +31,7 @@ eigenvalue_by_power_iteration, ) from ..utils.code_utils import deprecate_property -from ..utils.mat_utils import dip_azimuth2cartesian + from .. import optimization @@ -1338,13 +1338,14 @@ def start_irls(self): continue for obj in reg.objfcts: - threshold = np.percentile( - np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile + np.abs(obj.f_m(self.invProb.model)), self.prctile ) - - if isinstance(obj, SmoothnessFirstOrder): - threshold /= reg.regularization_mesh.base_length + # threshold = np.percentile( + # np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile + # ) + # if isinstance(obj, SmoothnessFirstOrder): + # threshold /= reg.regularization_mesh.base_length obj.irls_threshold = threshold @@ -1642,30 +1643,50 @@ def validate(self, directiveList): class UpdateReferenceVector(InversionDirective): - def __init__(self, regularization, azimuth, dip): + def __init__(self, regularization, reference_vector, component="direction"): self._regularization = regularization - self._reference = mkvc(dip_azimuth2cartesian(azimuth, dip)) + self._component = component + + if component == "direction": + self._reference = self.unit_vector(reference_vector) + else: + self._reference = self.get_amplitude(reference_vector) def initialize(self): - reference_vector = self.get_normalized_reference() - for objfct in self._regularization.objfcts: - objfct.reference_model = reference_vector + self.update_reference() def endIter(self): - reference_vector = self.get_normalized_reference() + self.update_reference() + + def update_reference(self): + n_comp = len(self._regularization.objfcts) + + if self._component == "direction": + amplitude = self.get_amplitude(self.invProb.model) + reference_vector = sdiag( + np.kron(np.ones(n_comp), amplitude) + ) * self._reference + else: + reference_vector = sdiag( + np.kron(np.ones(n_comp), self._reference) + ) * self.unit_vector(self.invProb.model) + for objfct in self._regularization.objfcts: objfct.reference_model = reference_vector - def get_normalized_reference(self): - n_comp = len(self._regularization.objfcts) + def get_amplitude(self, vector): model = [] for objfct in self._regularization.objfcts: - model.append(objfct.mapping * self.invProb.model) + model.append(objfct.mapping * vector) + + return np.linalg.norm(np.vstack(model).T, axis=1) - amplitude = np.linalg.norm(np.vstack(model).T, axis=1) + def unit_vector(self, vector): + inv_amp = (self.get_amplitude(vector) + 1e-12)**-1 return sdiag( - np.kron(np.ones(n_comp), amplitude) - ) * self._reference + np.kron(np.ones(len(self._regularization.objfcts)), inv_amp) + ) * vector + class ProjectSphericalBounds(InversionDirective): """ From 32609cb53a8c7140478b6074321b4fd48e5a6c56 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Oct 2022 09:29:29 -0700 Subject: [PATCH 018/455] Update example --- .../plot_inv_mag_MVI_VectorAmplitude.py | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 828f6d0844..8ec1d40a21 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -219,7 +219,7 @@ def plotVectorSectionsOctree( model_azm_dip = np.zeros((mesh.nC, 2)) -model_amp = np.zeros(mesh.nC) +model_amp = np.ones(mesh.nC) * 1e-8 for ii, anomaly in enumerate(M): x_shift = 120.0**ii # Get the indicies of the magnetized block @@ -319,13 +319,13 @@ def plotVectorSectionsOctree( wires = maps.Group(("p", nC), ("s", nC), ("t", nC)) -m0 = np.ones(3 * nC) * 1e-4 # Starting model +m0 = np.zeros(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components # of magnetization reg_amp = regularization.VectorAmplitude(mesh, wires, active_cells=actv) -reg_amp.reference_model = np.zeros(3*nC) -reg_amp.norms = [2, 0, 0, 0] +reg_amp.reference_model = np.zeros(3*nC)#mkvc(model)# +reg_amp.norms = [1, 0, 0, 0] # reg_amp.alpha_s = 0.0 reg_amp.gradient_type = "components" @@ -354,7 +354,30 @@ def plotVectorSectionsOctree( ) reg_components = reg_p + reg_s + reg_t -reg = reg_amp# + reg_components +reg_x = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.p, + reference_model_in_smooth=True, + norms=[0, 0, 0, 0] +) +reg_y = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.s, + reference_model_in_smooth=True, + norms=[0, 0, 0, 0] +) +reg_z = regularization.Sparse( + mesh, + active_cells=actv, + mapping=wires.t, + reference_model_in_smooth=True, + norms=[0, 0, 0, 0] +) +reg_amp = reg_x + reg_y + reg_z + +reg = reg_components + reg_amp# + # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -383,13 +406,18 @@ def plotVectorSectionsOctree( # Update reference model update_ref = directives.UpdateReferenceVector( - reg_components, model_azm_dip[:, 0], model_azm_dip[:, 1] + reg_components, mkvc(model), component="amplitude" + # reg_components, mkvc(utils.mat_utils.dip_azimuth2cartesian(model_azm_dip[:, 0], model_azm_dip[:, 1])) ) +update_dir = directives.UpdateReferenceVector( + reg_amp, mkvc(model), component="direction" +) inv = inversion.BaseInversion( invProb, directiveList=[ sensitivity_weights, update_ref, + update_dir, IRLS, update_Jacobi, betaest From 18fb28ae754e9e74e8f2ee8455d278ee97b1a68c Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 10 Oct 2022 22:11:49 -0700 Subject: [PATCH 019/455] Good runs with direction update on amplitude --- SimPEG/directives/directives.py | 23 ++-- .../plot_inv_mag_MVI_VectorAmplitude.py | 127 ++++++++++-------- 2 files changed, 83 insertions(+), 67 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 22092af6aa..0af206d5b1 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -1643,14 +1643,15 @@ def validate(self, directiveList): class UpdateReferenceVector(InversionDirective): - def __init__(self, regularization, reference_vector, component="direction"): + def __init__(self, regularization, mapping, component="direction"): self._regularization = regularization + self._mapping = mapping self._component = component - if component == "direction": - self._reference = self.unit_vector(reference_vector) + if component == "amplitude": + self._fixed_reference = self.unit_vector(regularization.objfcts[0].reference_model) else: - self._reference = self.get_amplitude(reference_vector) + self._fixed_reference = self.get_amplitude(regularization.objfcts[0].reference_model) def initialize(self): self.update_reference() @@ -1659,16 +1660,16 @@ def endIter(self): self.update_reference() def update_reference(self): - n_comp = len(self._regularization.objfcts) + n_comp = len(self._mapping.maps) - if self._component == "direction": + if self._component == "amplitude": amplitude = self.get_amplitude(self.invProb.model) reference_vector = sdiag( np.kron(np.ones(n_comp), amplitude) - ) * self._reference + ) * self._fixed_reference else: reference_vector = sdiag( - np.kron(np.ones(n_comp), self._reference) + np.kron(np.ones(n_comp), self._fixed_reference) ) * self.unit_vector(self.invProb.model) for objfct in self._regularization.objfcts: @@ -1676,15 +1677,15 @@ def update_reference(self): def get_amplitude(self, vector): model = [] - for objfct in self._regularization.objfcts: - model.append(objfct.mapping * vector) + for _, mapping in self._mapping.maps: + model.append(mapping * vector) return np.linalg.norm(np.vstack(model).T, axis=1) def unit_vector(self, vector): inv_amp = (self.get_amplitude(vector) + 1e-12)**-1 return sdiag( - np.kron(np.ones(len(self._regularization.objfcts)), inv_amp) + np.kron(np.ones(len(self._mapping.maps)), inv_amp) ) * vector diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 8ec1d40a21..a7a944275d 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -220,23 +220,32 @@ def plotVectorSectionsOctree( model_azm_dip = np.zeros((mesh.nC, 2)) model_amp = np.ones(mesh.nC) * 1e-8 -for ii, anomaly in enumerate(M): - x_shift = 120.0**ii - # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( - np.r_[-80 + x_shift, -20, -10], - np.r_[-40 + x_shift, 20, 25], - mesh.gridCC, - )[0] - - # Assign magnetization values - # model_azm_dip[ind, :] = np.kron(np.ones((ind.shape[0], 1)), anomaly) - model_amp[ind] = 0.05 - -model_azm_dip[mesh.cell_centers[:, 0] < 0, 0] = M[0][0, 0] -model_azm_dip[mesh.cell_centers[:, 0] < 0, 1] = M[0][0, 1] -model_azm_dip[mesh.cell_centers[:, 0] >= 0, 0] = M[1][0, 0] -model_azm_dip[mesh.cell_centers[:, 0] >= 0, 1] = M[1][0, 1] +# for ii, anomaly in enumerate(M): +# x_shift = 120.0**ii +# # Get the indicies of the magnetized block +# ind = utils.model_builder.getIndicesBlock( +# np.r_[-80 + x_shift, -20, -10], +# np.r_[-40 + x_shift, 20, 25], +# mesh.gridCC, +# )[0] +# +# # Assign magnetization values +# # model_azm_dip[ind, :] = np.kron(np.ones((ind.shape[0], 1)), anomaly) +# model_amp[ind] = 0.05 +# +# model_azm_dip[mesh.cell_centers[:, 0] < 0, 0] = M[0][0, 0] +# model_azm_dip[mesh.cell_centers[:, 0] < 0, 1] = M[0][0, 1] +# model_azm_dip[mesh.cell_centers[:, 0] >= 0, 0] = M[1][0, 0] +# model_azm_dip[mesh.cell_centers[:, 0] >= 0, 1] = M[1][0, 1] + +ind = utils.model_builder.getIndicesBlock( + np.r_[-30, -20, -10], + np.r_[30, 20, 25], + mesh.gridCC, +)[0] +model_amp[ind] = 0.05 +model_azm_dip[ind, 0] = 0. +model_azm_dip[ind, 1] = 90. # Remove air cells model_azm_dip = model_azm_dip[actv, :] @@ -319,15 +328,18 @@ def plotVectorSectionsOctree( wires = maps.Group(("p", nC), ("s", nC), ("t", nC)) -m0 = np.zeros(3 * nC) * 1e-4 # Starting model +m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components # of magnetization -reg_amp = regularization.VectorAmplitude(mesh, wires, active_cells=actv) -reg_amp.reference_model = np.zeros(3*nC)#mkvc(model)# -reg_amp.norms = [1, 0, 0, 0] +reg_amp = regularization.VectorAmplitude( + mesh, wires, active_cells=actv, + reference_model_in_smooth=True, +) +reg_amp.norms = [1.0, 0, 0, 0] # reg_amp.alpha_s = 0.0 reg_amp.gradient_type = "components" +reg_amp.reference_model = mkvc(model) # Create three regularizations for the different components # of magnetization @@ -335,49 +347,52 @@ def plotVectorSectionsOctree( mesh, active_cells=actv, mapping=wires.p, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] + reference_model_in_smooth=False, + norms=[0, 0, 0, 0], + alpha_s=0 ) reg_s = regularization.Sparse( mesh, active_cells=actv, mapping=wires.s, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] + reference_model_in_smooth=False, + norms=[0, 0, 0, 0], + alpha_s=0 ) reg_t = regularization.Sparse( mesh, active_cells=actv, mapping=wires.t, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] + reference_model_in_smooth=False, + norms=[0, 0, 0, 0], + alpha_s=0 ) reg_components = reg_p + reg_s + reg_t -reg_x = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.p, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] -) -reg_y = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.s, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] -) -reg_z = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.t, - reference_model_in_smooth=True, - norms=[0, 0, 0, 0] -) -reg_amp = reg_x + reg_y + reg_z +# reg_x = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.p, +# reference_model_in_smooth=True, +# norms=[0, 0, 0, 0] +# ) +# reg_y = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.s, +# reference_model_in_smooth=True, +# norms=[0, 0, 0, 0] +# ) +# reg_z = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.t, +# reference_model_in_smooth=True, +# norms=[0, 0, 0, 0] +# ) +# reg_amp = reg_x + reg_y + reg_z -reg = reg_components + reg_amp# + +reg = reg_amp# + reg_components #+ # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -405,18 +420,18 @@ def plotVectorSectionsOctree( update_Jacobi = directives.UpdatePreconditioner() # Update reference model -update_ref = directives.UpdateReferenceVector( - reg_components, mkvc(model), component="amplitude" - # reg_components, mkvc(utils.mat_utils.dip_azimuth2cartesian(model_azm_dip[:, 0], model_azm_dip[:, 1])) -) +# update_ref = directives.UpdateReferenceVector( +# reg_components, wires, component="amplitude" +# # reg_components, mkvc(utils.mat_utils.dip_azimuth2cartesian(model_azm_dip[:, 0], model_azm_dip[:, 1])) +# ) update_dir = directives.UpdateReferenceVector( - reg_amp, mkvc(model), component="direction" + reg_amp, wires, component="amplitude" ) inv = inversion.BaseInversion( invProb, directiveList=[ sensitivity_weights, - update_ref, + # update_ref, update_dir, IRLS, update_Jacobi, From ed41bc698f4a73ebdfd6938c76a10f0b0ca3d2d6 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 25 Oct 2022 10:51:22 -0700 Subject: [PATCH 020/455] Add a vector based regularization class, with a reference direction regularizer based on measure the cross-product --- SimPEG/directives/directives.py | 18 ++++-- SimPEG/regularization/__init__.py | 11 ++++ SimPEG/regularization/vector.py | 102 ++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 SimPEG/regularization/vector.py diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 30489e1e05..678e2d1a77 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -18,6 +18,7 @@ SmoothnessFirstOrder, SparseSmoothness, BaseSimilarityMeasure, + BaseVectorRegularization, ) from ..utils import ( mkvc, @@ -2302,16 +2303,25 @@ def update(self): wr = np.zeros_like(self.invProb.model) for reg in self.reg.objfcts: if not isinstance(reg, BaseSimilarityMeasure): - wr += reg.mapping.deriv(self.invProb.model).T * ( - (reg.mapping * jtj_diag) / reg.regularization_mesh.vol ** 2.0 - ) + mesh = reg.regularization_mesh + mapped_jtj_diag = reg.mapping * jtj_diag + if mapped_jtj_diag.size // mesh.dim > 1: + mapped_jtj_diag = mapped_jtj_diag.reshape((mesh.nC, -1), order="F") + wr_temp = ( + mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 + ) + wr_temp = wr_temp.reshape(-1, order="F") + else: + wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol ** 2 + wr += reg.mapping.deriv(self.invProb.model).T * wr_temp if self.normalization: wr /= wr.max() wr += self.threshold wr **= 0.5 for reg in self.reg.objfcts: if not isinstance(reg, BaseSimilarityMeasure): - for sub_reg in reg.objfcts: + sub_regs = getattr(reg, "objfcts", [reg]) + for sub_reg in sub_regs: sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) def validate(self, directiveList): diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 3bbadef098..47299654a3 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -127,6 +127,14 @@ SparseSmallness SparseSmoothness +Vector Regularizations +---------------------- +The regularizations are meant for models of vectors. + +.. autosummary:: + :toctree: generated/ + CrossReferenceRegularization + Joint Regularizations --------------------- There are several joint inversion regularizers available @@ -149,6 +157,7 @@ BaseRegularization BaseSimilarityMeasure BaseSparse + BaseVectorRegularization """ from ..utils.code_utils import deprecate_class @@ -167,6 +176,8 @@ from .correspondence import LinearCorrespondence from .jtv import JointTotalVariation +from .vector import BaseVectorRegularization, CrossReferenceRegularization + @deprecate_class(removal_version="0.19.0", future_warn=True) class SimpleSmall(Smallness): diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py new file mode 100644 index 0000000000..46c20a3294 --- /dev/null +++ b/SimPEG/regularization/vector.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +# Regularizations for vector models. + +import scipy.sparse as sp +import numpy as np +from .base import BaseRegularization, Smallness + + +class BaseVectorRegularization(BaseRegularization): + """The regularizers work on models where each value is a vector.""" + + @property + def _weights_shapes(self) -> tuple[int] | str: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + mesh = self.regularization_mesh + return [(mesh.nC,), (mesh.dim * mesh.nC,), (mesh.nC, mesh.dim)] + + +class CrossReferenceRegularization(Smallness, BaseVectorRegularization): + def __init__( + self, mesh, ref_dir, active_cells=None, mapping=None, weights=None, **kwargs + ): + kwargs.pop("reference_model", None) + super().__init__( + mesh=mesh, + active_cells=active_cells, + mapping=mapping, + weights=weights, + **kwargs, + ) + self.ref_dir = ref_dir + self.reference_model = 0.0 + + @property + def _nC_residual(self): + return np.prod(self.ref_dir.shape) + + @property + def ref_dir(self): + return self._ref_dir + + @ref_dir.setter + def ref_dir(self, value): + mesh = self.regularization_mesh + nC = mesh.nC + value = np.asarray(value) + if value.shape != (nC, mesh.dim): + if value.shape == (mesh.dim,): + # expand it out for each mesh cell + ref_dir = np.tile(value, (nC, 1)) + else: + raise ValueError(f"ref_dir must be shape {(nC, mesh.dim)}") + self._ref_dir = ref_dir + + R0 = sp.diags(ref_dir[:, 0]) + R1 = sp.diags(ref_dir[:, 1]) + if ref_dir.shape[1] == 2: + X = sp.bmat([[R1, -R0]]) + elif ref_dir.shape[1] == 3: + Z = sp.csr_matrix((nC, nC)) + R2 = sp.diags(ref_dir[:, 2]) + X = sp.bmat( + [ + [Z, R2, -R1], + [-R2, Z, R0], + [R1, -R0, Z], + ] + ) + self._X = X + + def f_m(self, m): + return self._X @ (self.mapping * m) + + def f_m_deriv(self, m): + return self._X @ self.mapping.deriv(m) + + @property + def W(self): + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh + nC = mesh.nC + + weights = np.ones( + nC, + ) + for value in self._weights.values(): + if value.shape == (nC,): + weights *= value + elif value.size == (mesh.dim * nC,): + weights *= np.linalg.norm( + value.reshape((nC, mesh.dim), order="F"), axis=1 + ) + weights = np.sqrt(weights) + self._W = sp.diags(np.r_[weights, weights, weights], format="csr") + return self._W From 7873708a75d79d22535c4768c62f994c866ac7fb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 4 Nov 2022 09:50:03 -0700 Subject: [PATCH 021/455] fix for direction input --- SimPEG/regularization/vector.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 46c20a3294..5eca871765 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -54,18 +54,18 @@ def ref_dir(self, value): if value.shape != (nC, mesh.dim): if value.shape == (mesh.dim,): # expand it out for each mesh cell - ref_dir = np.tile(value, (nC, 1)) + value = np.tile(value, (nC, 1)) else: raise ValueError(f"ref_dir must be shape {(nC, mesh.dim)}") - self._ref_dir = ref_dir + self._ref_dir = value - R0 = sp.diags(ref_dir[:, 0]) - R1 = sp.diags(ref_dir[:, 1]) - if ref_dir.shape[1] == 2: + R0 = sp.diags(value[:, 0]) + R1 = sp.diags(value[:, 1]) + if value.shape[1] == 2: X = sp.bmat([[R1, -R0]]) - elif ref_dir.shape[1] == 3: + elif value.shape[1] == 3: Z = sp.csr_matrix((nC, nC)) - R2 = sp.diags(ref_dir[:, 2]) + R2 = sp.diags(value[:, 2]) X = sp.bmat( [ [Z, R2, -R1], From 46af68cd17ebb61c4dfba2df76e79eb63200bcb9 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 25 Jan 2023 16:20:23 -0800 Subject: [PATCH 022/455] Bring back Group map --- SimPEG/maps.py | 21 ++++++++ SimPEG/regularization/vector_amplitude.py | 5 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 53 ++++++++++--------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 9a8d3b62aa..27bf4dc775 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -6121,3 +6121,24 @@ def deriv(self, m, v=None): else: out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) return out + + +class Group(Wires, IdentityMap): + def __init__(self, *args): + super(Group, self).__init__(*args) + + # Remove wires that are not part of the group + self._maps = ((name, wire) for (name, wire) in self.maps if name != "_") + self._nP = int(np.sum([wire.shape[0] for (_, wire) in self.maps])) + + def deriv(self, m, v=None): + """ + :param numpy.ndarray m: model + :rtype: scipy.sparse.csr_matrix + :return: derivative of transformed model + """ + deriv = [] + for name, wire in self.maps: + deriv += [wire.deriv(m, v)] + + return deriv \ No newline at end of file diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 9bb98f5f62..a883ab4e84 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -16,7 +16,7 @@ SparseSmoothnessFirstOrder ) from .. import utils -from SimPEG.utils.code_utils import validate_array_type, validate_shape +from SimPEG.utils.code_utils import validate_ndarray_with_shape if TYPE_CHECKING: from scipy.sparse import csr_matrix @@ -75,8 +75,7 @@ def set_weights(self, **weights): self._weights[key] = {} for (name, _), value in zip(self.mapping.maps, values): - validate_array_type("weights", value, float) - validate_shape("weights", value, self._weights_shapes) + validate_ndarray_with_shape("weights", value, shape=self._weights_shapes, dtype=float) self._weights[key][name] = value self._W = None diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index a7a944275d..4ec0fc5618 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -12,6 +12,7 @@ from discretize import TreeMesh from SimPEG import ( + dask, data, data_misfit, directives, @@ -263,7 +264,7 @@ def plotVectorSectionsOctree( # Create the simulation simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, chiMap=idenMap, actInd=actv, model_type="vector" + survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" ) # Compute some data and add some random noise @@ -343,31 +344,31 @@ def plotVectorSectionsOctree( # Create three regularizations for the different components # of magnetization -reg_p = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.p, - reference_model_in_smooth=False, - norms=[0, 0, 0, 0], - alpha_s=0 -) -reg_s = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.s, - reference_model_in_smooth=False, - norms=[0, 0, 0, 0], - alpha_s=0 -) -reg_t = regularization.Sparse( - mesh, - active_cells=actv, - mapping=wires.t, - reference_model_in_smooth=False, - norms=[0, 0, 0, 0], - alpha_s=0 -) -reg_components = reg_p + reg_s + reg_t +# reg_p = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.p, +# reference_model_in_smooth=False, +# norms=[0, 0, 0, 0], +# alpha_s=0 +# ) +# reg_s = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.s, +# reference_model_in_smooth=False, +# norms=[0, 0, 0, 0], +# alpha_s=0 +# ) +# reg_t = regularization.Sparse( +# mesh, +# active_cells=actv, +# mapping=wires.t, +# reference_model_in_smooth=False, +# norms=[0, 0, 0, 0], +# alpha_s=0 +# ) +# reg_components = reg_p + reg_s + reg_t # reg_x = regularization.Sparse( # mesh, From 17948ad669fdd63e95439768fc8cefd9acc3c300 Mon Sep 17 00:00:00 2001 From: fourndo Date: Tue, 31 Jan 2023 09:07:43 -0500 Subject: [PATCH 023/455] Fix example --- examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 4ec0fc5618..44885a7467 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -245,7 +245,7 @@ def plotVectorSectionsOctree( mesh.gridCC, )[0] model_amp[ind] = 0.05 -model_azm_dip[ind, 0] = 0. +model_azm_dip[ind, 0] = 45. model_azm_dip[ind, 1] = 90. # Remove air cells @@ -337,9 +337,9 @@ def plotVectorSectionsOctree( mesh, wires, active_cells=actv, reference_model_in_smooth=True, ) -reg_amp.norms = [1.0, 0, 0, 0] +reg_amp.norms = [0.0, 0, 0, 0] # reg_amp.alpha_s = 0.0 -reg_amp.gradient_type = "components" +reg_amp.gradient_type = "total" reg_amp.reference_model = mkvc(model) # Create three regularizations for the different components @@ -427,7 +427,7 @@ def plotVectorSectionsOctree( # ) update_dir = directives.UpdateReferenceVector( - reg_amp, wires, component="amplitude" + reg_amp, wires, component="direction" ) inv = inversion.BaseInversion( invProb, directiveList=[ From c488eaadb62ba7ce614a3c391cff9d1885fa616d Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 27 Feb 2023 15:36:25 -0800 Subject: [PATCH 024/455] Add multiprocessing version of the MetaSim --- SimPEG/meta/__init__.py | 2 + SimPEG/meta/multiprocessing.py | 228 +++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 SimPEG/meta/multiprocessing.py diff --git a/SimPEG/meta/__init__.py b/SimPEG/meta/__init__.py index 0e85cc886a..cdfe169250 100644 --- a/SimPEG/meta/__init__.py +++ b/SimPEG/meta/__init__.py @@ -1 +1,3 @@ from .simulation import MetaSimulation, SumMetaSimulation, RepeatedSimulation + +from .multiprocessing import MultiprocessingMetaSimulation diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py new file mode 100644 index 0000000000..ae08732d2d --- /dev/null +++ b/SimPEG/meta/multiprocessing.py @@ -0,0 +1,228 @@ +from multiprocessing import Process, Queue, cpu_count +from SimPEG.meta import MetaSimulation +from SimPEG.props import HasModel +import uuid +import numpy as np + + +class SimpleFuture: + def __init__(self, item_id, t_queue, r_queue): + self.item_id = item_id + self.t_queue = t_queue + self.r_queue = r_queue + + def result(self): + self.t_queue.put(("get_item", (self.item_id,))) + item = self.r_queue.get() + if isinstance(item, Exception): + raise item + return item + + def __del__(self): + self.t_queue.put(("del_item", (self.item_id,))) + + +class _SimulationProcess(Process): + """A very simple Simulation Actor process. + + It essentially encloses a single simulation in a process that will + then respond to requests to perform operations with its simulation. + it will also cache field objects created on this process instead of + returning them to the main processes, unless explicitly asked for... + """ + + def __init__(self, sim_chunk): + self.sim_chunk = sim_chunk + self.task_queue = Queue() + self.result_queue = Queue() + super().__init__() + + def run(self): + # everything here is local to the process + # this sim is actually local to the running process and will + # persist between calls to field, dprec, jvec,... + sim = self.sim_chunk + # a place to cache the field items locally + _cached_items = {} + + # The queues are shared between the head process and the worker processes + t_queue = self.task_queue + r_queue = self.result_queue + while True: + if not t_queue.empty(): + # Get a task from the queue + task = t_queue.get() + if task is None: + break + op, args = task + if op == "get_item": + (key,) = args + try: + r_queue.put(_cached_items[key]) + except Exception as err: + r_queue.put(err) + elif op == "del_item": + (key,) = args + _cached_items.pop(key, None) + else: + if op == 0: + # store_model + (m,) = args + sim.model = m + elif op == 1: + # create fields + f_key = uuid.uuid4().hex + r_queue.put(f_key) + fields = sim.fields(sim.model) + _cached_items[f_key] = fields + elif op == 2: + # do dpred + (f_key,) = args + fields = _cached_items[f_key] + r_queue.put(sim.dpred(sim.model, fields)) + elif op == 3: + # do jvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jvec(sim.model, v, fields)) + elif op == 4: + # do jtvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jtvec(sim.model, v, fields)) + elif op == 5: + # do jtj_diag + f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.getJtJdiag(sim.model, fields)) + + def store_model(self, m): + self.task_queue.put((0, (m,))) + + def get_fields(self): + self.task_queue.put((1, None)) + key = self.result_queue.get() + future = SimpleFuture(key, self.task_queue, self.result_queue) + return future + + def start_dpred(self, f_future): + self.task_queue.put((2, (f_future.item_id,))) + + def start_j_vec(self, v, f_future): + self.task_queue.put((3, (v, f_future.item_id))) + + def start_jt_vec(self, v, f_future): + self.task_queue.put((4, (v, f_future.item_id))) + + def start_jtj_diag(self, f_future): + self.task_queue.put((4, (f_future.item_id,))) + + +class MultiprocessingMetaSimulation(MetaSimulation): + def __init__(self, simulations, mappings, n_processes=None): + super().__init__(simulations, mappings) + + if n_processes is None: + n_processes = cpu_count() + + # split simulation,mappings up into chunks + # (Which are currently defined using MetaSimulations) + n_sim = len(simulations) + chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] + for i in range(n_sim % n_processes): + chunk_sizes[i] += 1 + + processes = [] + i_start = 0 + chunk_nd = [] + for chunk in chunk_sizes: + if chunk == 0: + continue + i_end = i_start + chunk + sim_chunk = MetaSimulation( + self.simulations[i_start:i_end], self.mappings[i_start:i_end] + ) + chunk_nd.append(sim_chunk.survey.nD) + p = _SimulationProcess(sim_chunk) + processes.append(p) + p.start() + i_start = i_end + + self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) + self._sim_processes = processes + self._is_alive = True + + def _check_alive(self): + if not self._is_alive: + raise RuntimeError( + "Child processes have already been killed. This simulation is now invalid." + ) + + @MetaSimulation.model.setter + def model(self, value): + self._check_alive() + updated = HasModel.model.fset(self, value) + # Only send the model to the internal simulations if it was updated. + if updated: + for p in self._sim_processes: + p.store_model(self._model) + + def fields(self, m): + self._check_alive() + self.model = m + # The above should pass the model to all the internal simulations. + f = [] + for p in self._sim_processes: + f.append(p.get_fields()) + return f + + def dpred(self, m=None, f=None): + self._check_alive() + if f is None: + if m is None: + m = self.model + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_dpred(field) + + d_pred = [] + for p in self._sim_processes: + d_pred.append(p.result_queue.get()) + return np.concatenate(d_pred) + + def Jvec(self, m, v, f=None): + self._check_alive() + self.model = m + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_j_vec(v, field) + j_vec = [] + for p in self._sim_processes: + j_vec.append(p.result_queue.get()) + return np.concatenate(j_vec) + + def Jtvec(self, m, v, f=None): + self._check_alive() + self.model = m + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + chunk_v = v[self._data_offsets[i] : self._data_offsets[i + 1]] + p.start_jt_vec(chunk_v, field) + + jt_vec = 0 + for p in self._sim_processes: + jt_vec += p.result_queue.get() + return jt_vec + + def join(self): + self._is_alive = False + # kill the subprocesses + for p in self._sim_processes: + if p.is_alive(): + p.task_queue.put(None) + p.join() + + def __del__(self): + self.join() From 572ee8cb9054c7af5c554991692926b6857c285e Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 1 Mar 2023 10:10:19 -0800 Subject: [PATCH 025/455] Make calls on the process check if alive --- SimPEG/meta/multiprocessing.py | 147 +++++++++++++++++---------------- 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index ae08732d2d..89219d188e 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -11,14 +11,19 @@ def __init__(self, item_id, t_queue, r_queue): self.t_queue = t_queue self.r_queue = r_queue - def result(self): - self.t_queue.put(("get_item", (self.item_id,))) - item = self.r_queue.get() - if isinstance(item, Exception): - raise item - return item + # This doesn't quite work well yet, + # Due to the fact that some fields objects from the PDE + # classes stash the simulation, so this requires serializing + # the simulation (something we explicitly want to avoid in all cases). + # def result(self): + # self.t_queue.put(("get_item", (self.item_id,))) + # item = self.r_queue.get() + # if isinstance(item, Exception): + # raise item + # return item def __del__(self): + # Tell the child process that this object is no longer needed in its cache. self.t_queue.put(("del_item", (self.item_id,))) @@ -46,76 +51,87 @@ def run(self): _cached_items = {} # The queues are shared between the head process and the worker processes + # We use them to communicate between the two. t_queue = self.task_queue r_queue = self.result_queue while True: - if not t_queue.empty(): - # Get a task from the queue - task = t_queue.get() - if task is None: - break - op, args = task - if op == "get_item": - (key,) = args - try: - r_queue.put(_cached_items[key]) - except Exception as err: - r_queue.put(err) - elif op == "del_item": - (key,) = args - _cached_items.pop(key, None) - else: - if op == 0: - # store_model - (m,) = args - sim.model = m - elif op == 1: - # create fields - f_key = uuid.uuid4().hex - r_queue.put(f_key) - fields = sim.fields(sim.model) - _cached_items[f_key] = fields - elif op == 2: - # do dpred - (f_key,) = args - fields = _cached_items[f_key] - r_queue.put(sim.dpred(sim.model, fields)) - elif op == 3: - # do jvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jvec(sim.model, v, fields)) - elif op == 4: - # do jtvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jtvec(sim.model, v, fields)) - elif op == 5: - # do jtj_diag - f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.getJtJdiag(sim.model, fields)) + # Get a task from the queue + task = t_queue.get() + if task is None: + # None is a poison pill message to kill this loop. + break + op, args = task + if op == "get_item": + (key,) = args + try: + r_queue.put(_cached_items[key]) + except Exception as err: + r_queue.put(err) + elif op == "del_item": + (key,) = args + _cached_items.pop(key, None) + else: + if op == 0: + # store_model + (m,) = args + sim.model = m + elif op == 1: + # create fields + f_key = uuid.uuid4().hex + r_queue.put(f_key) + fields = sim.fields(sim.model) + _cached_items[f_key] = fields + elif op == 2: + # do dpred + (f_key,) = args + fields = _cached_items[f_key] + r_queue.put(sim.dpred(sim.model, fields)) + elif op == 3: + # do jvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jvec(sim.model, v, fields)) + elif op == 4: + # do jtvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jtvec(sim.model, v, fields)) + elif op == 5: + # do jtj_diag + f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.getJtJdiag(sim.model, fields)) def store_model(self, m): + self._check_closed() self.task_queue.put((0, (m,))) def get_fields(self): + self._check_closed() self.task_queue.put((1, None)) key = self.result_queue.get() future = SimpleFuture(key, self.task_queue, self.result_queue) return future def start_dpred(self, f_future): + self._check_closed() self.task_queue.put((2, (f_future.item_id,))) def start_j_vec(self, v, f_future): + self._check_closed() self.task_queue.put((3, (v, f_future.item_id))) def start_jt_vec(self, v, f_future): + self._check_closed() self.task_queue.put((4, (v, f_future.item_id))) def start_jtj_diag(self, f_future): - self.task_queue.put((4, (f_future.item_id,))) + self._check_closed() + self.task_queue.put((5, (f_future.item_id,))) + + def result(self): + self._check_closed() + return self.result_queue.get() class MultiprocessingMetaSimulation(MetaSimulation): @@ -150,17 +166,9 @@ def __init__(self, simulations, mappings, n_processes=None): self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) self._sim_processes = processes - self._is_alive = True - - def _check_alive(self): - if not self._is_alive: - raise RuntimeError( - "Child processes have already been killed. This simulation is now invalid." - ) @MetaSimulation.model.setter def model(self, value): - self._check_alive() updated = HasModel.model.fset(self, value) # Only send the model to the internal simulations if it was updated. if updated: @@ -168,7 +176,6 @@ def model(self, value): p.store_model(self._model) def fields(self, m): - self._check_alive() self.model = m # The above should pass the model to all the internal simulations. f = [] @@ -177,7 +184,6 @@ def fields(self, m): return f def dpred(self, m=None, f=None): - self._check_alive() if f is None: if m is None: m = self.model @@ -187,11 +193,10 @@ def dpred(self, m=None, f=None): d_pred = [] for p in self._sim_processes: - d_pred.append(p.result_queue.get()) + d_pred.append(p.result()) return np.concatenate(d_pred) def Jvec(self, m, v, f=None): - self._check_alive() self.model = m if f is None: f = self.fields(m) @@ -199,11 +204,10 @@ def Jvec(self, m, v, f=None): p.start_j_vec(v, field) j_vec = [] for p in self._sim_processes: - j_vec.append(p.result_queue.get()) + j_vec.append(p.result()) return np.concatenate(j_vec) def Jtvec(self, m, v, f=None): - self._check_alive() self.model = m if f is None: f = self.fields(m) @@ -213,16 +217,15 @@ def Jtvec(self, m, v, f=None): jt_vec = 0 for p in self._sim_processes: - jt_vec += p.result_queue.get() + jt_vec += p.result() return jt_vec - def join(self): - self._is_alive = False - # kill the subprocesses + def close(self): for p in self._sim_processes: if p.is_alive(): p.task_queue.put(None) p.join() + p.close() def __del__(self): - self.join() + self.close() From 4f41c634c18b636baab8286134cfcbc56868c003 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 1 Mar 2023 14:06:58 -0800 Subject: [PATCH 026/455] Add beginnings of documentation for class --- SimPEG/meta/multiprocessing.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 89219d188e..e56a046a2f 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -135,6 +135,26 @@ def result(self): class MultiprocessingMetaSimulation(MetaSimulation): + """Multiprocessing version of simulation of simulations. + + This class makes use of the `multiprocessing` module to provide + concurrency, executing the internal simulations in parallel. + + If using this class, please be conscious of your operating system's + default method of spawning new processes. On Windows systems this + means that the user must be sure that this code is only executed on + the main process. Usually this is solved in your main script by + protecting your function calls by checking if you are in `__main__` + with: + + >>> from SimPEG.meta import MultiprocessingMetaSimulation + >>> if __name__ == '__main__': + ... # Do processing here + ... sim = MultiprocessingMetaSimulation(...) + ... sim.dpred(model) + + """ + def __init__(self, simulations, mappings, n_processes=None): super().__init__(simulations, mappings) From afe3f0a04838e5bd137c8c8a9e0d340b14acf81c Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 29 Mar 2023 11:23:02 -0700 Subject: [PATCH 027/455] Update example --- examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 44885a7467..65df9d7fb6 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -337,10 +337,10 @@ def plotVectorSectionsOctree( mesh, wires, active_cells=actv, reference_model_in_smooth=True, ) -reg_amp.norms = [0.0, 0, 0, 0] +reg_amp.norms = [0.0, 1.0, 1.0, 1.0] # reg_amp.alpha_s = 0.0 reg_amp.gradient_type = "total" -reg_amp.reference_model = mkvc(model) +reg_amp.reference_model = mkvc(m0) # Create three regularizations for the different components # of magnetization From 004e33c85b69b1ec2d4bf7a05ad0125b052ef6b8 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 18:20:36 -0700 Subject: [PATCH 028/455] Remove unnecessary UpdateReference directive --- SimPEG/directives/__init__.py | 1 - SimPEG/directives/directives.py | 47 --------------------------------- 2 files changed, 48 deletions(-) diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py index 3ef349dd73..aed37d2e10 100644 --- a/SimPEG/directives/__init__.py +++ b/SimPEG/directives/__init__.py @@ -17,7 +17,6 @@ JointScalingSchedule, UpdateSensitivityWeights, ProjectSphericalBounds, - UpdateReferenceVector ) from .pgi_directives import ( diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 2cff6b5cfa..6cef9245d2 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2368,53 +2368,6 @@ def validate(self, directiveList): return True -class UpdateReferenceVector(InversionDirective): - def __init__(self, regularization, mapping, component="direction"): - self._regularization = regularization - self._mapping = mapping - self._component = component - - if component == "amplitude": - self._fixed_reference = self.unit_vector(regularization.objfcts[0].reference_model) - else: - self._fixed_reference = self.get_amplitude(regularization.objfcts[0].reference_model) - - def initialize(self): - self.update_reference() - - def endIter(self): - self.update_reference() - - def update_reference(self): - n_comp = len(self._mapping.maps) - - if self._component == "amplitude": - amplitude = self.get_amplitude(self.invProb.model) - reference_vector = sdiag( - np.kron(np.ones(n_comp), amplitude) - ) * self._fixed_reference - else: - reference_vector = sdiag( - np.kron(np.ones(n_comp), self._fixed_reference) - ) * self.unit_vector(self.invProb.model) - - for objfct in self._regularization.objfcts: - objfct.reference_model = reference_vector - - def get_amplitude(self, vector): - model = [] - for _, mapping in self._mapping.maps: - model.append(mapping * vector) - - return np.linalg.norm(np.vstack(model).T, axis=1) - - def unit_vector(self, vector): - inv_amp = (self.get_amplitude(vector) + 1e-12)**-1 - return sdiag( - np.kron(np.ones(len(self._mapping.maps)), inv_amp) - ) * vector - - class ProjectSphericalBounds(InversionDirective): r""" Trick for spherical coordinate system. From 09d60f170bc3ecb86ff195d01e0e1825975593bf Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 18:32:07 -0700 Subject: [PATCH 029/455] Allow inject inactive on 2D array --- SimPEG/maps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 27bf4dc775..603fe7db9b 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -3130,6 +3130,8 @@ def nP(self): return int(self.indActive.sum()) def _transform(self, m): + if m.ndim > 1: + return self.P * m + self.valInactive[:, None] return self.P * m + self.valInactive def inverse(self, u): From 7fd46c112c883480809a037917799ef973a9af3c Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 18:32:34 -0700 Subject: [PATCH 030/455] Make example rely on plot_slice --- .../plot_inv_mag_MVI_VectorAmplitude.py | 293 ++++-------------- 1 file changed, 68 insertions(+), 225 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 65df9d7fb6..2253ba48e3 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -121,95 +121,6 @@ # Should eventually end up on discretize # - -def plotVectorSectionsOctree( - mesh, - m, - normal="X", - ind=0, - vmin=None, - vmax=None, - scale=1.0, - vec="k", - axs=None, - actvMap=None, - fill=True, -): - - """ - Plot section through a 3D tensor model - """ - # plot recovered model - normalInd = {"X": 0, "Y": 1, "Z": 2}[normal] - antiNormalInd = {"X": [1, 2], "Y": [0, 2], "Z": [0, 1]}[normal] - - h2d = (mesh.h[antiNormalInd[0]], mesh.h[antiNormalInd[1]]) - x2d = (mesh.x0[antiNormalInd[0]], mesh.x0[antiNormalInd[1]]) - - #: Size of the sliced dimension - szSliceDim = len(mesh.h[normalInd]) - if ind is None: - ind = int(szSliceDim // 2) - - cc_tensor = [None, None, None] - for i in range(3): - cc_tensor[i] = np.cumsum(np.r_[mesh.x0[i], mesh.h[i]]) - cc_tensor[i] = (cc_tensor[i][1:] + cc_tensor[i][:-1]) * 0.5 - slice_loc = cc_tensor[normalInd][ind] - - # Create a temporary TreeMesh with the slice through - temp_mesh = TreeMesh(h2d, x2d) - level_diff = mesh.max_level - temp_mesh.max_level - - XS = [None, None, None] - XS[antiNormalInd[0]], XS[antiNormalInd[1]] = np.meshgrid( - cc_tensor[antiNormalInd[0]], cc_tensor[antiNormalInd[1]] - ) - XS[normalInd] = np.ones_like(XS[antiNormalInd[0]]) * slice_loc - loc_grid = np.c_[XS[0].reshape(-1), XS[1].reshape(-1), XS[2].reshape(-1)] - inds = np.unique(mesh._get_containing_cell_indexes(loc_grid)) - - grid2d = mesh.gridCC[inds][:, antiNormalInd] - levels = mesh._cell_levels_by_indexes(inds) - level_diff - temp_mesh.insert_cells(grid2d, levels) - tm_gridboost = np.empty((temp_mesh.nC, 3)) - tm_gridboost[:, antiNormalInd] = temp_mesh.gridCC - tm_gridboost[:, normalInd] = slice_loc - - # Interpolate values to mesh.gridCC if not 'CC' - mx = actvMap * m[:, 0] - my = actvMap * m[:, 1] - mz = actvMap * m[:, 2] - - m = np.c_[mx, my, mz] - - # Interpolate values from mesh.gridCC to grid2d - ind_3d_to_2d = mesh._get_containing_cell_indexes(tm_gridboost) - v2d = m[ind_3d_to_2d, :] - amp = np.sum(v2d ** 2.0, axis=1) ** 0.5 - v2d = sdiag(1./amp) * v2d - if axs is None: - axs = plt.subplot(111) - - if fill: - temp_mesh.plotImage(amp, ax=axs, clim=[vmin, vmax], grid=False) - - axs.quiver( - temp_mesh.gridCC[:, 0], - temp_mesh.gridCC[:, 1], - v2d[:, antiNormalInd[0]], - v2d[:, antiNormalInd[1]], - pivot="mid", - scale_units="inches", - scale=scale, - linewidths=(1,), - edgecolors=(vec), - headaxislength=0.1, - headwidth=10, - headlength=30, - ) - - ########################################################################### # Forward modeling data # --------------------- @@ -217,28 +128,8 @@ def plotVectorSectionsOctree( # We can now create a magnetization model and generate data # Lets start with a block below topography # - - model_azm_dip = np.zeros((mesh.nC, 2)) model_amp = np.ones(mesh.nC) * 1e-8 -# for ii, anomaly in enumerate(M): -# x_shift = 120.0**ii -# # Get the indicies of the magnetized block -# ind = utils.model_builder.getIndicesBlock( -# np.r_[-80 + x_shift, -20, -10], -# np.r_[-40 + x_shift, 20, 25], -# mesh.gridCC, -# )[0] -# -# # Assign magnetization values -# # model_azm_dip[ind, :] = np.kron(np.ones((ind.shape[0], 1)), anomaly) -# model_amp[ind] = 0.05 -# -# model_azm_dip[mesh.cell_centers[:, 0] < 0, 0] = M[0][0, 0] -# model_azm_dip[mesh.cell_centers[:, 0] < 0, 1] = M[0][0, 1] -# model_azm_dip[mesh.cell_centers[:, 0] >= 0, 0] = M[1][0, 0] -# model_azm_dip[mesh.cell_centers[:, 0] >= 0, 1] = M[1][0, 1] - ind = utils.model_builder.getIndicesBlock( np.r_[-30, -20, -10], np.r_[30, 20, 25], @@ -256,6 +147,10 @@ def plotVectorSectionsOctree( model_azm_dip[:, 0], model_azm_dip[:, 1] ) +mref_false = sdiag(model_amp) * utils.mat_utils.dip_azimuth2cartesian( + model_azm_dip[:, 0], model_azm_dip[:, 1] * 3. +) + # Create active map to go from reduce set to full actvMap = maps.InjectActiveCells(mesh, actv, np.nan) @@ -280,33 +175,37 @@ def plotVectorSectionsOctree( actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) # Plot the model and data -# plt.figure() -# ax = plt.subplot(2, 1, 1) -# im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) -# plt.colorbar(im[0]) -# ax.set_title("Predicted data.") -# plt.gca().set_aspect("equal", adjustable="box") -# -# # Plot the vector model -# ax = plt.subplot(2, 1, 2) -# plotVectorSectionsOctree( -# mesh, -# model, -# axs=ax, -# normal="Y", -# ind=66, -# actvMap=actv_plot, -# scale=10, -# vmin=0.0, -# vmax=0.025, -# ) -# ax.set_xlim([-200, 200]) -# ax.set_ylim([-100, 75]) -# ax.set_xlabel("x") -# ax.set_ylabel("y") -# plt.gca().set_aspect("equal", adjustable="box") -# -# plt.show() +plt.figure() +ax = plt.subplot(2, 1, 1) +im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) +plt.colorbar(im[0]) +ax.set_title("Predicted data.") +plt.gca().set_aspect("equal", adjustable="box") + +# Plot the vector model +ax = plt.subplot(2, 1, 2) +mesh.plot_slice( + actv_plot * mref_false.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, + normal="Y", + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 0.5, + "angles": 'xy', + "scale_units": 'height' + }, +) +ax.set_xlim([-200, 200]) +ax.set_ylim([-100, 75]) +ax.set_xlabel("x") +ax.set_ylabel("y") +plt.gca().set_aspect("equal", adjustable="box") + +plt.show() ###################################################################### @@ -327,73 +226,20 @@ def plotVectorSectionsOctree( # This Mapping connects the regularizations for the three-component # vector model wires = maps.Group(("p", nC), ("s", nC), ("t", nC)) - - m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components # of magnetization reg_amp = regularization.VectorAmplitude( - mesh, wires, active_cells=actv, + mesh, + wires, + active_cells=actv, reference_model_in_smooth=True, + norms=[0.0, 1.0, 1.0, 1.0], + reference_model=m0 ) -reg_amp.norms = [0.0, 1.0, 1.0, 1.0] -# reg_amp.alpha_s = 0.0 -reg_amp.gradient_type = "total" -reg_amp.reference_model = mkvc(m0) - -# Create three regularizations for the different components -# of magnetization -# reg_p = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.p, -# reference_model_in_smooth=False, -# norms=[0, 0, 0, 0], -# alpha_s=0 -# ) -# reg_s = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.s, -# reference_model_in_smooth=False, -# norms=[0, 0, 0, 0], -# alpha_s=0 -# ) -# reg_t = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.t, -# reference_model_in_smooth=False, -# norms=[0, 0, 0, 0], -# alpha_s=0 -# ) -# reg_components = reg_p + reg_s + reg_t - -# reg_x = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.p, -# reference_model_in_smooth=True, -# norms=[0, 0, 0, 0] -# ) -# reg_y = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.s, -# reference_model_in_smooth=True, -# norms=[0, 0, 0, 0] -# ) -# reg_z = regularization.Sparse( -# mesh, -# active_cells=actv, -# mapping=wires.t, -# reference_model_in_smooth=True, -# norms=[0, 0, 0, 0] -# ) -# reg_amp = reg_x + reg_y + reg_z -reg = reg_amp# + reg_components #+ +reg = reg_amp # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) @@ -407,7 +253,7 @@ def plotVectorSectionsOctree( invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) # A list of directive to control the inverson -betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) +betaest = directives.BetaEstimate_ByEig(beta0_ratio=5e1) # Add sensitivity weights sensitivity_weights = directives.UpdateSensitivityWeights() @@ -420,20 +266,10 @@ def plotVectorSectionsOctree( # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() -# Update reference model -# update_ref = directives.UpdateReferenceVector( -# reg_components, wires, component="amplitude" -# # reg_components, mkvc(utils.mat_utils.dip_azimuth2cartesian(model_azm_dip[:, 0], model_azm_dip[:, 1])) -# ) -update_dir = directives.UpdateReferenceVector( - reg_amp, wires, component="direction" -) inv = inversion.BaseInversion( invProb, directiveList=[ sensitivity_weights, - # update_ref, - update_dir, IRLS, update_Jacobi, betaest @@ -455,16 +291,20 @@ def plotVectorSectionsOctree( plt.figure(figsize=(8, 8)) ax = plt.subplot(2, 1, 1) -plotVectorSectionsOctree( - mesh, - invProb.l2model.reshape((nC, 3), order="F"), - axs=ax, +mesh.plot_slice( + actv_plot * invProb.l2model.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=10, - vmin=0.0, - # vmax=0.01, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 0.5, + "angles": 'xy', + "scale_units": 'height' + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -474,17 +314,20 @@ def plotVectorSectionsOctree( plt.gca().set_aspect("equal", adjustable="box") ax = plt.subplot(2, 1, 2) - -plotVectorSectionsOctree( - mesh, - mrec_MVIC.reshape((nC, 3), order="F"), - axs=ax, +mesh.plot_slice( + actv_plot * mrec_MVIC.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=10.0, - vmin=0.0, - # vmax=0.025, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 0.5, + "angles": 'xy', + "scale_units": 'height' + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) From 828cd6b1362999b149203cebef6bf586f9309b7b Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 19:58:02 -0700 Subject: [PATCH 031/455] Fix vector plot length --- .../plot_inv_mag_MVI_VectorAmplitude.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 2253ba48e3..377fbe49d4 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -142,18 +142,10 @@ # Remove air cells model_azm_dip = model_azm_dip[actv, :] model_amp = model_amp[actv] - model = sdiag(model_amp) * utils.mat_utils.dip_azimuth2cartesian( model_azm_dip[:, 0], model_azm_dip[:, 1] ) -mref_false = sdiag(model_amp) * utils.mat_utils.dip_azimuth2cartesian( - model_azm_dip[:, 0], model_azm_dip[:, 1] * 3. -) - -# Create active map to go from reduce set to full -actvMap = maps.InjectActiveCells(mesh, actv, np.nan) - # Creat reduced identity map idenMap = maps.IdentityMap(nP=nC * 3) @@ -171,7 +163,7 @@ # Assign data and uncertainties to the survey data_object = data.Data(survey, dobs=synthetic_data, standard_deviation=wd) -# Create an projection matrix for plotting later +# Create a projection matrix for plotting later actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) # Plot the model and data @@ -185,7 +177,7 @@ # Plot the vector model ax = plt.subplot(2, 1, 2) mesh.plot_slice( - actv_plot * mref_false.reshape((-1, 3), order="F"), + actv_plot * model.reshape((-1, 3), order="F"), v_type="CCv", view="vec", ax=ax, @@ -194,9 +186,8 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 0.5, - "angles": 'xy', - "scale_units": 'height' + "scale": 0.1, + "scale_units": 'inches' }, ) ax.set_xlim([-200, 200]) @@ -301,9 +292,8 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 0.5, - "angles": 'xy', - "scale_units": 'height' + "scale": 4*np.abs(invProb.l2model).max(), + "scale_units": 'inches' }, ) ax.set_xlim([-200, 200]) @@ -324,9 +314,8 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 0.5, - "angles": 'xy', - "scale_units": 'height' + "scale": 4*np.abs(mrec_MVIC).max(), + "scale_units": 'inches' }, ) ax.set_xlim([-200, 200]) @@ -338,6 +327,7 @@ plt.show() +print("END") # Plot the final predicted data and the residual # plt.figure() # ax = plt.subplot(1, 2, 1) From 3a9a00ef78ad440b290df5c593b67ed1297c578b Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 20:01:33 -0700 Subject: [PATCH 032/455] One more time --- examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 377fbe49d4..919d9e8862 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -186,7 +186,7 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 0.1, + "scale": 5 * np.abs(model).max(), "scale_units": 'inches' }, ) @@ -292,7 +292,7 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 4*np.abs(invProb.l2model).max(), + "scale": 5 * np.abs(invProb.l2model).max(), "scale_units": 'inches' }, ) @@ -314,7 +314,7 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 4*np.abs(mrec_MVIC).max(), + "scale": 5 * np.abs(mrec_MVIC).max(), "scale_units": 'inches' }, ) From 29bae4f8fb73c8df10d9394f5053dd12e1a97d7c Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 20:36:14 -0700 Subject: [PATCH 033/455] Revert base reg changes --- SimPEG/maps.py | 1 + SimPEG/regularization/base.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index bdde98ff2e..5757778be4 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -6196,6 +6196,7 @@ def __init__(self, *args): super(Group, self).__init__(*args) # Remove wires that are not part of the group + self._mesh = None self._maps = ((name, wire) for (name, wire) in self.maps if name != "_") self._nP = int(np.sum([wire.shape[0] for (_, wire) in self.maps])) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 7e16796329..263fc64f8e 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -286,8 +286,8 @@ def _nC_residual(self) -> int: nC = getattr(self.regularization_mesh, "nC", None) mapping = getattr(self, "_mapping", None) - if mapping is not None and mapping.nP != "*": - return self.mapping.nP + if mapping is not None and mapping.shape[1] != "*": + return self.mapping.shape[1] if nC != "*" and nC is not None: return self.regularization_mesh.nC From bb6ecec9d5e0bfc08a58ada9b323b5a575e231f3 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 21:01:23 -0700 Subject: [PATCH 034/455] Revert name change for SparseSmoothness --- SimPEG/directives/directives.py | 4 ++-- SimPEG/directives/pgi_directives.py | 6 +++--- SimPEG/regularization/__init__.py | 2 +- SimPEG/regularization/sparse.py | 8 ++++---- SimPEG/regularization/vector_amplitude.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index f03bfc760e..ae6d35f0c1 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -16,7 +16,7 @@ PGIsmallness, PGIwithNonlinearRelationshipsSmallness, SmoothnessFirstOrder, - SparseSmoothnessFirstOrder, + SparseSmoothness, BaseSimilarityMeasure, ) from ..utils import ( @@ -715,7 +715,7 @@ def initialize(self): ): smallness += [obj] - elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)): + elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothness)): parents[obj] = regobjcts smoothness += [obj] diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index c20f520f06..dbcfec45f8 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -13,7 +13,7 @@ PGI, PGIsmallness, SmoothnessFirstOrder, - SparseSmoothnessFirstOrder, + SparseSmoothness, PGIwithRelationships, ) from ..utils import ( @@ -348,7 +348,7 @@ def initialize(self): [ i, j, - isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)), + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)), ] ] self.Smooth = np.r_[Smooth] @@ -365,7 +365,7 @@ def initialize(self): self.nbr = len(self.reg.objfcts) self.Smooth = np.r_[ [ - isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothnessFirstOrder)) + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)) for regpart in self.reg.objfcts ] ] diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index fa54830ce7..c609e99cd1 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -192,7 +192,7 @@ SmoothnessSecondOrder, ) from .regularization_mesh import RegularizationMesh -from .sparse import BaseSparse, SparseSmallness, SparseSmoothnessFirstOrder, Sparse +from .sparse import BaseSparse, SparseSmallness, SparseSmoothness, Sparse from .pgi import PGIsmallness, PGI from .cross_gradient import CrossGradient from .correspondence import LinearCorrespondence diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 8bdf9ffe66..414db98fdd 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -124,7 +124,7 @@ def update_weights(self, m): self.set_weights(irls=self.get_lp_weights(f_m)) -class SparseSmoothnessFirstOrder(BaseSparse, SmoothnessFirstOrder): +class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): """ Base Class for sparse regularization on first spatial derivatives """ @@ -242,14 +242,14 @@ def __init__( if objfcts is None: objfcts = [ SparseSmallness(mesh=self.regularization_mesh), - SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="x"), + SparseSmoothness(mesh=self.regularization_mesh, orientation="x"), ] if mesh.dim > 1: - objfcts.append(SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="y")) + objfcts.append(SparseSmoothness(mesh=self.regularization_mesh, orientation="y")) if mesh.dim > 2: - objfcts.append(SparseSmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="z")) + objfcts.append(SparseSmoothness(mesh=self.regularization_mesh, orientation="z")) gradientType = kwargs.pop("gradientType", None) super().__init__( diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index a883ab4e84..9c185c4d08 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -13,7 +13,7 @@ from .sparse import ( Sparse, SparseSmallness, - SparseSmoothnessFirstOrder + SparseSmoothness ) from .. import utils from SimPEG.utils.code_utils import validate_ndarray_with_shape @@ -149,7 +149,7 @@ def W(self): return self._W -class AmplitudeSmoothnessFirstOrder(SparseSmoothnessFirstOrder, BaseAmplitude): +class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): """ Base Class for sparse regularization on first spatial derivatives """ From bc1018720f7b9320beddf78115fab5ab9d09ca7a Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 21:20:42 -0700 Subject: [PATCH 035/455] Cleanup change to directives --- SimPEG/directives/directives.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index ae6d35f0c1..c7c694fb84 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2190,11 +2190,8 @@ def start_irls(self): threshold = np.percentile( np.abs(obj.f_m(self.invProb.model)), self.prctile ) - # threshold = np.percentile( - # np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile - # ) - # if isinstance(obj, SmoothnessFirstOrder): - # threshold /= reg.regularization_mesh.base_length + if isinstance(obj, SmoothnessFirstOrder): + threshold /= reg.regularization_mesh.base_length obj.irls_threshold = threshold From 4c77d7dc270b21590bd7c2d076cec3a97e480e52 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 1 Apr 2023 21:31:59 -0700 Subject: [PATCH 036/455] Run black --- SimPEG/directives/directives.py | 5 +- SimPEG/directives/pgi_directives.py | 4 +- SimPEG/maps.py | 2 +- SimPEG/regularization/sparse.py | 8 ++- SimPEG/regularization/vector_amplitude.py | 59 +++++++++++++---------- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index c7c694fb84..6bc3d6ed6d 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2722,11 +2722,11 @@ def update(self): if hasattr(reg.mapping, "maps"): for _, wire in reg.mapping.maps: wr += wire.deriv(self.invProb.model).T * ( - (wire * jtj_diag) / reg.regularization_mesh.vol ** 2.0 + (wire * jtj_diag) / reg.regularization_mesh.vol**2.0 ) else: wr += reg.mapping.deriv(self.invProb.model).T * ( - (reg.mapping * jtj_diag) / reg.regularization_mesh.vol ** 2.0 + (reg.mapping * jtj_diag) / reg.regularization_mesh.vol**2.0 ) if self.normalization: @@ -2758,7 +2758,6 @@ def update(self): for sub_reg in sub_regs: sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) - def validate(self, directiveList): """Validate directive against directives list. diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index dbcfec45f8..66a33b3ea6 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -348,7 +348,9 @@ def initialize(self): [ i, j, - isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)), + isinstance( + regpart, (SmoothnessFirstOrder, SparseSmoothness) + ), ] ] self.Smooth = np.r_[Smooth] diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 5757778be4..bf43d0a574 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -6210,4 +6210,4 @@ def deriv(self, m, v=None): for name, wire in self.maps: deriv += [wire.deriv(m, v)] - return deriv \ No newline at end of file + return deriv diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 414db98fdd..127df8dd31 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -246,10 +246,14 @@ def __init__( ] if mesh.dim > 1: - objfcts.append(SparseSmoothness(mesh=self.regularization_mesh, orientation="y")) + objfcts.append( + SparseSmoothness(mesh=self.regularization_mesh, orientation="y") + ) if mesh.dim > 2: - objfcts.append(SparseSmoothness(mesh=self.regularization_mesh, orientation="z")) + objfcts.append( + SparseSmoothness(mesh=self.regularization_mesh, orientation="z") + ) gradientType = kwargs.pop("gradientType", None) super().__init__( diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 9c185c4d08..fcdd4c625f 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -6,15 +6,8 @@ from discretize.base import BaseMesh from SimPEG import maps -from .base import ( - RegularizationMesh, - BaseRegularization -) -from .sparse import ( - Sparse, - SparseSmallness, - SparseSmoothness -) +from .base import RegularizationMesh, BaseRegularization +from .sparse import Sparse, SparseSmallness, SparseSmoothness from .. import utils from SimPEG.utils.code_utils import validate_ndarray_with_shape @@ -26,6 +19,7 @@ class BaseAmplitude(BaseRegularization): """ Base vector amplitude function. """ + _W = None def __init__(self, mesh, **kwargs): @@ -71,36 +65,37 @@ def set_weights(self, **weights): values = (values,) * len(self.mapping.maps) if len(values) != len(self.mapping.maps): - raise ValueError(f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})") + raise ValueError( + f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})" + ) self._weights[key] = {} for (name, _), value in zip(self.mapping.maps, values): - validate_ndarray_with_shape("weights", value, shape=self._weights_shapes, dtype=float) + validate_ndarray_with_shape( + "weights", value, shape=self._weights_shapes, dtype=float + ) self._weights[key][name] = value self._W = None @utils.timeIt def __call__(self, m): - """ - """ + """ """ r = self.W * self.f_m(m) return 0.5 * r.dot(r) @utils.timeIt def deriv(self, m) -> np.ndarray: - """ - """ - f_m_derivs = 0. + """ """ + f_m_derivs = 0.0 for f_m_deriv in self.f_m_deriv(m): f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * m) return f_m_derivs @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: - """ - """ - f_m_derivs = 0. + """ """ + f_m_derivs = 0.0 for f_m_deriv in self.f_m_deriv(m): if v is None: f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) @@ -215,10 +210,13 @@ def update_weights(self, m): if self.units is not None and self.units.lower() == "radian": Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") length_scales = Ave * ( - self.regularization_mesh.Pac.T - * self.regularization_mesh.mesh.h_gridded[:, ii] + self.regularization_mesh.Pac.T + * self.regularization_mesh.mesh.h_gridded[:, ii] + ) + dm = ( + utils.mat_utils.coterminal(dm * length_scales) + / length_scales ) - dm = utils.mat_utils.coterminal(dm * length_scales) / length_scales f_m += np.abs( getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm @@ -260,14 +258,24 @@ def __init__( objfcts = [ AmplitudeSmallness(mesh=self.regularization_mesh, mapping=wire_map), - AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="x"), + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, mapping=wire_map, orientation="x" + ), ] if mesh.dim > 1: - objfcts.append(AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="y")) + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, mapping=wire_map, orientation="y" + ) + ) if mesh.dim > 2: - objfcts.append(AmplitudeSmoothnessFirstOrder(mesh=self.regularization_mesh, mapping=wire_map, orientation="z")) + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, mapping=wire_map, orientation="z" + ) + ) super().__init__( self.regularization_mesh, @@ -294,4 +302,3 @@ def mapping(self, wires): for fct in self.objfcts: fct.mapping = wires - From ee54bf1ca42a358db58448ea4df89f5af7eebd22 Mon Sep 17 00:00:00 2001 From: fourndo Date: Fri, 7 Apr 2023 09:27:44 -0700 Subject: [PATCH 037/455] Fix active cells on exmaple --- examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 919d9e8862..0c8f3a2c90 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -26,7 +26,7 @@ from SimPEG import utils from SimPEG.utils import mkvc, sdiag -from discretize.utils import mesh_builder_xyz, refine_tree_xyz +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz from SimPEG.potential_fields import magnetics import scipy as sp import numpy as np @@ -112,7 +112,7 @@ # Define an active cells from topo -actv = utils.surface2ind_topo(mesh, topo) +actv = active_from_xyz(mesh, topo) nC = int(actv.sum()) ########################################################################### From 278b50aec4d0b6768535160deecf31c5f6043075 Mon Sep 17 00:00:00 2001 From: fourndo Date: Fri, 7 Apr 2023 10:28:51 -0700 Subject: [PATCH 038/455] Get away without Group map --- SimPEG/maps.py | 22 ---------------------- SimPEG/regularization/vector_amplitude.py | 23 +++++++++++++++++++---- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/SimPEG/maps.py b/SimPEG/maps.py index bf43d0a574..fe579c9b33 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -6189,25 +6189,3 @@ def deriv(self, m, v=None): else: out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) return out - - -class Group(Wires, IdentityMap): - def __init__(self, *args): - super(Group, self).__init__(*args) - - # Remove wires that are not part of the group - self._mesh = None - self._maps = ((name, wire) for (name, wire) in self.maps if name != "_") - self._nP = int(np.sum([wire.shape[0] for (_, wire) in self.maps])) - - def deriv(self, m, v=None): - """ - :param numpy.ndarray m: model - :rtype: scipy.sparse.csr_matrix - :return: derivative of transformed model - """ - deriv = [] - for name, wire in self.maps: - deriv += [wire.deriv(m, v)] - - return deriv diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index fcdd4c625f..9374e090e1 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -104,6 +104,16 @@ def deriv2(self, m, v=None) -> csr_matrix: return f_m_derivs + @property + def _nC_residual(self) -> int: + """ + Shape of the residual + """ + if self.mapping is None: + raise AttributeError("The regularization does not have a 'mapping' yet.") + + return int(np.sum([wire.shape[0] for (_, wire) in self.mapping.maps])) + class AmplitudeSmallness(SparseSmallness, BaseAmplitude): """ @@ -123,7 +133,11 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: - return self.mapping.deriv(self._delta_m(m)) + deriv = [] + dm = self._delta_m(m) + for name, wire in self.mapping.maps: + deriv += [wire.deriv(dm)] + return deriv @property def W(self): @@ -157,8 +171,9 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] - for map_deriv in self.mapping.deriv(self._delta_m(m)): - deriv.append(self.cell_gradient * map_deriv) + dm = self._delta_m(m) + for name, wire in self.mapping.maps: + deriv += [self.cell_gradient * wire.deriv(dm)] return deriv @@ -301,4 +316,4 @@ def mapping(self, wires): self._mapping = wires for fct in self.objfcts: - fct.mapping = wires + fct.mapping = wires \ No newline at end of file From 0d9fee8880f4f68acfd19a67ec9166ed47fa4f1e Mon Sep 17 00:00:00 2001 From: fourndo Date: Fri, 7 Apr 2023 11:16:10 -0700 Subject: [PATCH 039/455] Update Amplitude example. Update plots for Spherical example --- SimPEG/directives/directives.py | 2 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 155 ++++-------------- .../plot_inv_mag_MVI_VectorAmplitude.py | 5 +- 3 files changed, 38 insertions(+), 124 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 6bc3d6ed6d..07497f1793 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2719,7 +2719,7 @@ def update(self): if isinstance(reg, BaseSimilarityMeasure): continue - if hasattr(reg.mapping, "maps"): + if isinstance(reg.mapping, Wires): for _, wire in reg.mapping.maps: wr += wire.deriv(self.invProb.model).T * ( (wire * jtj_diag) / reg.regularization_mesh.vol**2.0 diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index cc9176afdd..840e1f3a46 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -117,100 +117,6 @@ actv = active_from_xyz(mesh, topo) nC = int(actv.sum()) -########################################################################### -# A simple function to plot vectors in TreeMesh -# -# Should eventually end up on discretize -# - - -def plotVectorSectionsOctree( - mesh, - m, - normal="X", - ind=0, - vmin=None, - vmax=None, - scale=1.0, - vec="k", - axs=None, - actvMap=None, - fill=True, -): - """ - Plot section through a 3D tensor model - """ - # plot recovered model - normalInd = {"X": 0, "Y": 1, "Z": 2}[normal] - antiNormalInd = {"X": [1, 2], "Y": [0, 2], "Z": [0, 1]}[normal] - - h2d = (mesh.h[antiNormalInd[0]], mesh.h[antiNormalInd[1]]) - x2d = (mesh.x0[antiNormalInd[0]], mesh.x0[antiNormalInd[1]]) - - #: Size of the sliced dimension - szSliceDim = len(mesh.h[normalInd]) - if ind is None: - ind = int(szSliceDim // 2) - - cc_tensor = [None, None, None] - for i in range(3): - cc_tensor[i] = np.cumsum(np.r_[mesh.x0[i], mesh.h[i]]) - cc_tensor[i] = (cc_tensor[i][1:] + cc_tensor[i][:-1]) * 0.5 - slice_loc = cc_tensor[normalInd][ind] - - # Create a temporary TreeMesh with the slice through - temp_mesh = TreeMesh(h2d, x2d) - level_diff = mesh.max_level - temp_mesh.max_level - - XS = [None, None, None] - XS[antiNormalInd[0]], XS[antiNormalInd[1]] = np.meshgrid( - cc_tensor[antiNormalInd[0]], cc_tensor[antiNormalInd[1]] - ) - XS[normalInd] = np.ones_like(XS[antiNormalInd[0]]) * slice_loc - loc_grid = np.c_[XS[0].reshape(-1), XS[1].reshape(-1), XS[2].reshape(-1)] - inds = np.unique(mesh._get_containing_cell_indexes(loc_grid)) - - grid2d = mesh.gridCC[inds][:, antiNormalInd] - levels = mesh._cell_levels_by_indexes(inds) - level_diff - temp_mesh.insert_cells(grid2d, levels) - tm_gridboost = np.empty((temp_mesh.nC, 3)) - tm_gridboost[:, antiNormalInd] = temp_mesh.gridCC - tm_gridboost[:, normalInd] = slice_loc - - # Interpolate values to mesh.gridCC if not 'CC' - mx = actvMap * m[:, 0] - my = actvMap * m[:, 1] - mz = actvMap * m[:, 2] - - m = np.c_[mx, my, mz] - - # Interpolate values from mesh.gridCC to grid2d - ind_3d_to_2d = mesh._get_containing_cell_indexes(tm_gridboost) - v2d = m[ind_3d_to_2d, :] - amp = np.sum(v2d**2.0, axis=1) ** 0.5 - - if axs is None: - axs = plt.subplot(111) - - if fill: - temp_mesh.plot_image(amp, ax=axs, clim=[vmin, vmax], grid=True) - - axs.quiver( - temp_mesh.gridCC[:, 0], - temp_mesh.gridCC[:, 1], - v2d[:, antiNormalInd[0]], - v2d[:, antiNormalInd[1]], - pivot="mid", - scale_units="inches", - scale=scale, - linewidths=(1,), - edgecolors=(vec), - headaxislength=0.1, - headwidth=10, - headlength=30, - ) - - ########################################################################### # Forward modeling data # --------------------- @@ -271,16 +177,19 @@ def plotVectorSectionsOctree( # Plot the vector model ax = plt.subplot(2, 1, 2) -plotVectorSectionsOctree( - mesh, - model, - axs=ax, +mesh.plot_slice( + actv_plot * model.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, normal="Y", ind=66, - actvMap=actv_plot, - scale=0.5, - vmin=0.0, - vmax=0.025, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(model).max(), + "scale_units": 'inches' + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -456,16 +365,19 @@ def plotVectorSectionsOctree( plt.figure(figsize=(8, 8)) ax = plt.subplot(2, 1, 1) -plotVectorSectionsOctree( - mesh, +mesh.plot_slice( mrec_MVIC.reshape((nC, 3), order="F"), - axs=ax, + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=0.05, - vmin=0.0, - vmax=0.005, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(invProb.l2model).max(), + "scale_units": 'inches' + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -476,23 +388,26 @@ def plotVectorSectionsOctree( ax = plt.subplot(2, 1, 2) vec_xyz = utils.mat_utils.spherical2cartesian( - invProb.model.reshape((nC, 3), order="F") + mrec_MVI_S.reshape((nC, 3), order="F") ).reshape((nC, 3), order="F") -plotVectorSectionsOctree( - mesh, +mesh.plot_slice( vec_xyz, - axs=ax, + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=0.4, - vmin=0.0, - vmax=0.025, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(mrec_MVIC).max(), + "scale_units": 'inches' + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) -ax.set_title("Sparse model (Spherical)") +ax.set_title("Sparse model (L0L2)") ax.set_xlabel("x") ax.set_ylabel("y") plt.gca().set_aspect("equal", adjustable="box") diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 0c8f3a2c90..1ef52f33dc 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -10,7 +10,6 @@ """ -from discretize import TreeMesh from SimPEG import ( dask, data, @@ -216,7 +215,7 @@ # This Mapping connects the regularizations for the three-component # vector model -wires = maps.Group(("p", nC), ("s", nC), ("t", nC)) +wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create three regularizations for the different components @@ -244,7 +243,7 @@ invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) # A list of directive to control the inverson -betaest = directives.BetaEstimate_ByEig(beta0_ratio=5e1) +betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) # Add sensitivity weights sensitivity_weights = directives.UpdateSensitivityWeights() From aa88dc152e4c7644c7ad9cb9ae058efb2565afd5 Mon Sep 17 00:00:00 2001 From: fourndo Date: Fri, 7 Apr 2023 13:12:14 -0700 Subject: [PATCH 040/455] Add docstrings to Amplitude reg --- SimPEG/regularization/vector_amplitude.py | 16 +++++++++ .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 35 ++++++++++++------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 9374e090e1..24a90879c9 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -248,6 +248,22 @@ def update_weights(self, m): class VectorAmplitude(Sparse): """ The regularization is: + + The function defined here approximates: + + .. math:: + \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ + + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ + + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ + + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ + + where $\mathbf{a}(\mathbf{m} - \mathbf{m_{ref})$ is the vector amplitude of the difference between + the model and the reference model. + + .. math:: + \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} + + where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. ... """ diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 840e1f3a46..01fcdd8cae 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -289,26 +289,35 @@ # Create a Combo Regularization # Regularize the amplitude of the vectors reg_a = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.amp + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.amp, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, + reference_model=np.zeros(3 * nC) ) -reg_a.norms = [0.0, 0.0, 0.0, 0.0] # Sparse on the model and its gradients -reg_a.reference_model = np.zeros(3 * nC) # Regularize the vertical angle of the vectors reg_t = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.theta + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.theta, + alpha_s=0.0, # No reference angle, + norms=[0.0, 1.0, 1.0, 1.0] # Only norm on gradients used, ) -reg_t.alpha_s = 0.0 # No reference angle reg_t.units = "radian" -reg_t.norms = [0.0, 0.0, 0.0, 0.0] # Only norm on gradients used # Regularize the horizontal angle of the vectors reg_p = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.phi + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.phi, + alpha_s=0.0, # No reference angle, + norms=[0.0, 1.0, 1.0, 1.0] # Only norm on gradients used, ) -reg_p.alpha_s = 0.0 # No reference angle reg_p.units = "radian" -reg_p.norms = [0.0, 0.0, 0.0, 0.0] # Only norm on gradients used reg = reg_a + reg_t + reg_p reg.reference_model = np.zeros(3 * nC) @@ -366,7 +375,7 @@ plt.figure(figsize=(8, 8)) ax = plt.subplot(2, 1, 1) mesh.plot_slice( - mrec_MVIC.reshape((nC, 3), order="F"), + actv_plot * mrec_MVIC.reshape((nC, 3), order="F"), v_type="CCv", view="vec", ax=ax, @@ -375,7 +384,7 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 5 * np.abs(invProb.l2model).max(), + "scale": 5 * np.abs(mrec_MVIC).max(), "scale_units": 'inches' }, ) @@ -392,7 +401,7 @@ ).reshape((nC, 3), order="F") mesh.plot_slice( - vec_xyz, + actv_plot * vec_xyz, v_type="CCv", view="vec", ax=ax, @@ -401,7 +410,7 @@ grid=True, quiver_opts={ "pivot": "mid", - "scale": 5 * np.abs(mrec_MVIC).max(), + "scale": 5 * np.abs(vec_xyz).max(), "scale_units": 'inches' }, ) From 70b36d9e3ecdc73e4833a845b7f408d95ca30a23 Mon Sep 17 00:00:00 2001 From: fourndo Date: Fri, 7 Apr 2023 13:34:26 -0700 Subject: [PATCH 041/455] Ignore .idea from pycharm --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0b177b79ab..9418df3e91 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ develop-eggs .installed.cfg lib lib64 +.idea __pycache__ # Installer logs From ad2851205eaaeece6efc8b3a5dd5578ff8d77f75 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 09:34:19 -0700 Subject: [PATCH 042/455] Further simplify implementation --- SimPEG/regularization/vector_amplitude.py | 110 ++++++---------------- 1 file changed, 27 insertions(+), 83 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 24a90879c9..0274de7c33 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -61,48 +61,51 @@ def set_weights(self, **weights): array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) """ for key, values in weights.items(): - if not isinstance(values, tuple): - values = (values,) * len(self.mapping.maps) + if isinstance(values, tuple): + if len(values) != len(self.mapping.maps): + raise ValueError( + f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})" + ) - if len(values) != len(self.mapping.maps): - raise ValueError( - f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})" - ) + for value in values: + validate_ndarray_with_shape( + "weights", value, shape=self._weights_shapes, dtype=float + ) - self._weights[key] = {} - for (name, _), value in zip(self.mapping.maps, values): + self._weights[key] = np.linalg.norm(np.vstack(values), axis=0) + else: validate_ndarray_with_shape( - "weights", value, shape=self._weights_shapes, dtype=float + "weights", values, shape=self._weights_shapes, dtype=float ) - self._weights[key][name] = value + self._weights[key] = values self._W = None - @utils.timeIt - def __call__(self, m): - """ """ - r = self.W * self.f_m(m) - return 0.5 * r.dot(r) - @utils.timeIt def deriv(self, m) -> np.ndarray: """ """ - f_m_derivs = 0.0 - for f_m_deriv in self.f_m_deriv(m): - f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * m) - return f_m_derivs + d_m = self._delta_m(m) + + deriv = 0. + + for f_m_deriv in self.f_m_deriv(d_m): + deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * d_m) + + return deriv @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: """ """ - f_m_derivs = 0.0 + deriv = 0. + for f_m_deriv in self.f_m_deriv(m): + if v is None: - f_m_derivs += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) else: - f_m_derivs += f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) + deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * v) - return f_m_derivs + return deriv @property def _nC_residual(self) -> int: @@ -139,24 +142,6 @@ def f_m_deriv(self, m) -> csr_matrix: deriv += [wire.deriv(dm)] return deriv - @property - def W(self): - """ - Weighting matrix - """ - if getattr(self, "_W", None) is None: - weights = [] - - for name, _ in self.mapping.maps: - weights.append(1.0) - for weight in self._weights.values(): - weights[-1] *= weight[name] - - weights[-1] = utils.sdiag(weights[-1] ** 0.5) - - self._W = sp.vstack(weights) - return self._W - class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): """ @@ -177,35 +162,6 @@ def f_m_deriv(self, m) -> csr_matrix: return deriv - @property - def W(self): - """ - Weighting matrix that takes the volumes, free weights, fixed weights and - length scales of the difference operator (normalized optional). - """ - if getattr(self, "_W", None) is None: - average_cell_2_face = getattr( - self.regularization_mesh, "aveCC2F{}".format(self.orientation) - ) - weights = [] - - for name, _ in self.mapping.maps: - - weights.append(1.0) - - for weight in self._weights.values(): - values = weight[name] - if values.shape[0] == self.regularization_mesh.nC: - values = average_cell_2_face * values - - weights[-1] *= values - - weights[-1] = utils.sdiag(weights[-1] ** 0.5) - - self._W = sp.vstack(weights) - - return self._W - def update_weights(self, m): """ Compute and store the irls weights. @@ -221,18 +177,6 @@ def update_weights(self, m): getattr(self.regularization_mesh, f"cell_gradient_{comp}") * delta_m ) - - if self.units is not None and self.units.lower() == "radian": - Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") - length_scales = Ave * ( - self.regularization_mesh.Pac.T - * self.regularization_mesh.mesh.h_gridded[:, ii] - ) - dm = ( - utils.mat_utils.coterminal(dm * length_scales) - / length_scales - ) - f_m += np.abs( getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm ) From 84b24eb7abbcaf8219613c493edfdc0b2c0cac3f Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 10:03:08 -0700 Subject: [PATCH 043/455] Revert import change --- SimPEG/directives/pgi_directives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index 66a33b3ea6..1ab80fc18d 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -12,9 +12,9 @@ from ..regularization import ( PGI, PGIsmallness, + PGIwithRelationships, SmoothnessFirstOrder, SparseSmoothness, - PGIwithRelationships, ) from ..utils import ( GaussianMixtureWithNonlinearRelationships, From 30c57ce22c3345880aea2564e42742d7a57d8527 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 11:03:41 -0700 Subject: [PATCH 044/455] Cleanup example --- .../plot_inv_mag_MVI_VectorAmplitude.py | 166 +++++------------- 1 file changed, 41 insertions(+), 125 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 1ef52f33dc..83286e8694 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -48,19 +48,12 @@ # We will assume a vertical inducing field H0 = (50000.0, 90.0, 0.0) -# The magnetization is set along a different direction (induced + remanence) -M = [ - np.c_[45.0, 270.0], - np.c_[45.0, 90.0], -] - # Create grid of points for topography # Lets create a simple Gaussian topo and set the active cells [xx, yy] = np.meshgrid(np.linspace(-200, 200, 50), np.linspace(-200, 200, 50)) b = 100 A = 50 zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0)) - topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] # Create an array of observation points @@ -75,27 +68,11 @@ srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) survey = magnetics.survey.Survey(srcField) -# Here how the topography looks with a quick interpolation, just a Gaussian... -tri = sp.spatial.Delaunay(topo) -# fig = plt.figure() -# ax = fig.add_subplot(1, 1, 1, projection="3d") -# ax.plot_trisurf( -# topo[:, 0], topo[:, 1], topo[:, 2], triangles=tri.simplices, cmap=plt.cm.Spectral -# ) -# ax.scatter3D(xyzLoc[:, 0], xyzLoc[:, 1], xyzLoc[:, 2], c="k") -# plt.show() - ############################################################################### # Inversion Mesh # -------------- # -# Here, we create a TreeMesh with base cell size of 5 m. We created a small -# utility function to center the mesh around points and to figure out the -# outermost dimension for adequate padding distance. -# The second stage allows us to refine the mesh around points or surfaces -# (point assumed to follow some horizontal trend) -# The refinement process is repeated twice to allow for a finer level around -# the survey locations. +# Here, we create a TreeMesh with base cell size of 5 m. # # Create a mesh @@ -114,18 +91,11 @@ actv = active_from_xyz(mesh, topo) nC = int(actv.sum()) -########################################################################### -# A simple function to plot vectors in TreeMesh -# -# Should eventually end up on discretize -# - ########################################################################### # Forward modeling data # --------------------- # -# We can now create a magnetization model and generate data -# Lets start with a block below topography +# We can now create a magnetization model and generate data. # model_azm_dip = np.zeros((mesh.nC, 2)) model_amp = np.ones(mesh.nC) * 1e-8 @@ -145,7 +115,7 @@ model_azm_dip[:, 0], model_azm_dip[:, 1] ) -# Creat reduced identity map +# Create reduced identity map idenMap = maps.IdentityMap(nP=nC * 3) # Create the simulation @@ -165,49 +135,12 @@ # Create a projection matrix for plotting later actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) -# Plot the model and data -plt.figure() -ax = plt.subplot(2, 1, 1) -im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) -plt.colorbar(im[0]) -ax.set_title("Predicted data.") -plt.gca().set_aspect("equal", adjustable="box") - -# Plot the vector model -ax = plt.subplot(2, 1, 2) -mesh.plot_slice( - actv_plot * model.reshape((-1, 3), order="F"), - v_type="CCv", - view="vec", - ax=ax, - normal="Y", - ind=66, - grid=True, - quiver_opts={ - "pivot": "mid", - "scale": 5 * np.abs(model).max(), - "scale_units": 'inches' - }, -) -ax.set_xlim([-200, 200]) -ax.set_ylim([-100, 75]) -ax.set_xlabel("x") -ax.set_ylabel("y") -plt.gca().set_aspect("equal", adjustable="box") - -plt.show() - ###################################################################### # Inversion # --------- # -# We can now attempt the inverse calculations. We put great care -# into designing an inversion methology that would yield a geologically -# reasonable solution for the non-induced problem. -# The inversion is done in two stages. First we compute a smooth -# solution using a Cartesian coordinate system, then a sparse -# inversion in the Spherical domain. +# We can now attempt the inverse calculations. # # Create sensitivity weights from our linear forward operator @@ -218,39 +151,34 @@ wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) m0 = np.ones(3 * nC) * 1e-4 # Starting model -# Create three regularizations for the different components -# of magnetization -reg_amp = regularization.VectorAmplitude( +# Create the regularization on the amplitude of magnetization +reg = regularization.VectorAmplitude( mesh, wires, active_cells=actv, reference_model_in_smooth=True, - norms=[0.0, 1.0, 1.0, 1.0], - reference_model=m0 + norms=[1.0, 0.0, 0.0, 0.0], ) -reg = reg_amp - # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) dmis.W = 1.0 / data_object.standard_deviation -# Add directives to the inversion +# The optimization scheme opt = optimization.ProjectedGNCG( maxIter=20, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 ) +# The inverse problem invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) -# A list of directive to control the inverson +# Estimate the initial beta factor betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) # Add sensitivity weights sensitivity_weights = directives.UpdateSensitivityWeights() # Here is where the norms are applied -# Use a threshold parameter empirically based on the distribution of -# model parameters IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) # Pre-conditioner @@ -267,7 +195,7 @@ ) # Run the inversion -mrec_MVIC = inv.run(m0) +mrec = inv.run(m0) ############################################################# @@ -279,50 +207,38 @@ # # -plt.figure(figsize=(8, 8)) -ax = plt.subplot(2, 1, 1) -mesh.plot_slice( - actv_plot * invProb.l2model.reshape((-1, 3), order="F"), - v_type="CCv", - view="vec", - ax=ax, - normal="Y", - ind=66, - grid=True, - quiver_opts={ - "pivot": "mid", - "scale": 5 * np.abs(invProb.l2model).max(), - "scale_units": 'inches' - }, -) -ax.set_xlim([-200, 200]) -ax.set_ylim([-100, 75]) -ax.set_title("Smooth model (Cartesian)") -ax.set_xlabel("x") -ax.set_ylabel("y") +plt.figure(figsize=(12, 6)) +ax = plt.subplot(2, 2, 1) +im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) +plt.colorbar(im[0]) +ax.set_title("Predicted data.") plt.gca().set_aspect("equal", adjustable="box") -ax = plt.subplot(2, 1, 2) -mesh.plot_slice( - actv_plot * mrec_MVIC.reshape((-1, 3), order="F"), - v_type="CCv", - view="vec", - ax=ax, - normal="Y", - ind=66, - grid=True, - quiver_opts={ - "pivot": "mid", - "scale": 5 * np.abs(mrec_MVIC).max(), - "scale_units": 'inches' - }, -) -ax.set_xlim([-200, 200]) -ax.set_ylim([-100, 75]) -ax.set_title("Sparse model (L0L2)") -ax.set_xlabel("x") -ax.set_ylabel("y") -plt.gca().set_aspect("equal", adjustable="box") +for ii, (title, mvec) in enumerate([ + ("True model", model), + ("Smooth model", invProb.l2model), + ("Sparse model", mrec) +]): + ax = plt.subplot(2, 2, ii + 2) + mesh.plot_slice( + actv_plot * mvec.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, + normal="Y", + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 8 * np.abs(mvec).max(), + "scale_units": 'inches' + }, + ) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_title(title) + ax.set_xlabel("x") + ax.set_ylabel("z") + plt.gca().set_aspect("equal", adjustable="box") plt.show() From c434f29574c33a0d83ea4e1ad6cb481926595721 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 14:13:02 -0700 Subject: [PATCH 045/455] Start adding unitests --- SimPEG/regularization/vector_amplitude.py | 34 +++++++++++++---------- tests/base/test_regularization.py | 25 +++++++++++++++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 0274de7c33..3303663820 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -30,9 +30,12 @@ def mapping(self) -> maps.Wires: return self._mapping @mapping.setter - def mapping(self, wires): - if not isinstance(wires, maps.Wires): - raise ValueError(f"A 'mapping' of type {maps.Wires} must be provided.") + def mapping(self, wires: maps.Wires | None): + if isinstance(wires, type(None)): + wires = maps.Wires(("model", self.regularization_mesh.nC)) + + elif not isinstance(wires, maps.Wires): + raise TypeError(f"A 'mapping' of type {maps.Wires} must be provided.") for wire in wires.maps: if wire[1].shape[0] != self.regularization_mesh.nC: @@ -214,48 +217,49 @@ class VectorAmplitude(Sparse): def __init__( self, mesh, - wire_map, + mapping=None, active_cells=None, **kwargs, ): - if not isinstance(mesh, RegularizationMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - TypeError( + if not isinstance(mesh, (BaseMesh, RegularizationMesh)): + raise TypeError( f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " f"Value of type {type(mesh)} provided." ) + + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + self._regularization_mesh = mesh if active_cells is not None: self._regularization_mesh.active_cells = active_cells objfcts = [ - AmplitudeSmallness(mesh=self.regularization_mesh, mapping=wire_map), + AmplitudeSmallness(mesh=self.regularization_mesh, mapping=mapping), AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, mapping=wire_map, orientation="x" + mesh=self.regularization_mesh, orientation="x", mapping=mapping ), ] if mesh.dim > 1: objfcts.append( AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, mapping=wire_map, orientation="y" + mesh=self.regularization_mesh, orientation="y", mapping=mapping ) ) if mesh.dim > 2: objfcts.append( AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, mapping=wire_map, orientation="z" + mesh=self.regularization_mesh, orientation="z", mapping=mapping ) ) super().__init__( self.regularization_mesh, objfcts=objfcts, - mapping=wire_map, + mapping=mapping, **kwargs, ) @@ -266,7 +270,7 @@ def mapping(self): @mapping.setter def mapping(self, wires): if not isinstance(wires, maps.Wires): - raise ValueError(f"A 'mapping' of type {maps.Wires} must be provided.") + raise TypeError(f"A 'mapping' of type {maps.Wires} must be provided.") for wire in wires.maps: if wire[1].shape[0] != self.regularization_mesh.nC: diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index cc4f4a00aa..0c279f2be8 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -27,6 +27,7 @@ "CrossGradient", "LinearCorrespondence", "JointTotalVariation", + "VectorAmplitude", ] @@ -549,6 +550,30 @@ def test_sparse_properties(self): assert reg.gradient_type == "total" # Check default + def test_vector_amplitude(self): + n_comp = 4 + mesh = discretize.TensorMesh([8, 7]) + model = np.random.randn(mesh.nC, n_comp) + + with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): + regularization.VectorAmplitude("abc") + + with pytest.raises(TypeError, match="A 'mapping' of type"): + regularization.VectorAmplitude( + mesh, maps.IdentityMap(mesh) + ) + + wires = ((f"wire{ind}", mesh.nC) for ind in range(n_comp)) + mapping = maps.Wires(*wires) + + reg = regularization.VectorAmplitude(mesh, mapping) + + np.testing.assert_almost_equal( + reg.objfcts[0].f_m(model.flatten(order='F')), + np.linalg.norm(model, axis=1) + ) + + if __name__ == "__main__": unittest.main() From dc35ec73ab2377fed89fd7010f1c1a102e17b060 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 14:14:11 -0700 Subject: [PATCH 046/455] Black fix ups --- SimPEG/regularization/vector_amplitude.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 3303663820..ce4e986c37 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -89,7 +89,7 @@ def deriv(self, m) -> np.ndarray: """ """ d_m = self._delta_m(m) - deriv = 0. + deriv = 0.0 for f_m_deriv in self.f_m_deriv(d_m): deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * d_m) @@ -99,7 +99,7 @@ def deriv(self, m) -> np.ndarray: @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: """ """ - deriv = 0. + deriv = 0.0 for f_m_deriv in self.f_m_deriv(m): @@ -280,4 +280,4 @@ def mapping(self, wires): self._mapping = wires for fct in self.objfcts: - fct.mapping = wires \ No newline at end of file + fct.mapping = wires From 6449b184ff3bd13fbf601e5cd127ee562f40b2c2 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 14:18:26 -0700 Subject: [PATCH 047/455] More black --- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 14 +++++------ .../plot_inv_mag_MVI_VectorAmplitude.py | 23 +++++++------------ tests/base/test_regularization.py | 8 ++----- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 01fcdd8cae..e0df4671e9 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -188,7 +188,7 @@ quiver_opts={ "pivot": "mid", "scale": 5 * np.abs(model).max(), - "scale_units": 'inches' + "scale_units": "inches", }, ) ax.set_xlim([-200, 200]) @@ -293,8 +293,8 @@ gradient_type="total", active_cells=actv, mapping=wires.amp, - norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, - reference_model=np.zeros(3 * nC) + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, + reference_model=np.zeros(3 * nC), ) # Regularize the vertical angle of the vectors @@ -304,7 +304,7 @@ active_cells=actv, mapping=wires.theta, alpha_s=0.0, # No reference angle, - norms=[0.0, 1.0, 1.0, 1.0] # Only norm on gradients used, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, ) reg_t.units = "radian" @@ -315,7 +315,7 @@ active_cells=actv, mapping=wires.phi, alpha_s=0.0, # No reference angle, - norms=[0.0, 1.0, 1.0, 1.0] # Only norm on gradients used, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, ) reg_p.units = "radian" @@ -385,7 +385,7 @@ quiver_opts={ "pivot": "mid", "scale": 5 * np.abs(mrec_MVIC).max(), - "scale_units": 'inches' + "scale_units": "inches", }, ) ax.set_xlim([-200, 200]) @@ -411,7 +411,7 @@ quiver_opts={ "pivot": "mid", "scale": 5 * np.abs(vec_xyz).max(), - "scale_units": 'inches' + "scale_units": "inches", }, ) ax.set_xlim([-200, 200]) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 83286e8694..05b150c948 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -105,8 +105,8 @@ mesh.gridCC, )[0] model_amp[ind] = 0.05 -model_azm_dip[ind, 0] = 45. -model_azm_dip[ind, 1] = 90. +model_azm_dip[ind, 0] = 45.0 +model_azm_dip[ind, 1] = 90.0 # Remove air cells model_azm_dip = model_azm_dip[actv, :] @@ -149,7 +149,7 @@ # This Mapping connects the regularizations for the three-component # vector model wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) -m0 = np.ones(3 * nC) * 1e-4 # Starting model +m0 = np.ones(3 * nC) * 1e-4 # Starting model # Create the regularization on the amplitude of magnetization reg = regularization.VectorAmplitude( @@ -186,12 +186,7 @@ inv = inversion.BaseInversion( - invProb, directiveList=[ - sensitivity_weights, - IRLS, - update_Jacobi, - betaest - ] + invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) # Run the inversion @@ -214,11 +209,9 @@ ax.set_title("Predicted data.") plt.gca().set_aspect("equal", adjustable="box") -for ii, (title, mvec) in enumerate([ - ("True model", model), - ("Smooth model", invProb.l2model), - ("Sparse model", mrec) -]): +for ii, (title, mvec) in enumerate( + [("True model", model), ("Smooth model", invProb.l2model), ("Sparse model", mrec)] +): ax = plt.subplot(2, 2, ii + 2) mesh.plot_slice( actv_plot * mvec.reshape((-1, 3), order="F"), @@ -230,7 +223,7 @@ quiver_opts={ "pivot": "mid", "scale": 8 * np.abs(mvec).max(), - "scale_units": 'inches' + "scale_units": "inches", }, ) ax.set_xlim([-200, 200]) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 0c279f2be8..5fd54d902b 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -559,9 +559,7 @@ def test_vector_amplitude(self): regularization.VectorAmplitude("abc") with pytest.raises(TypeError, match="A 'mapping' of type"): - regularization.VectorAmplitude( - mesh, maps.IdentityMap(mesh) - ) + regularization.VectorAmplitude(mesh, maps.IdentityMap(mesh)) wires = ((f"wire{ind}", mesh.nC) for ind in range(n_comp)) mapping = maps.Wires(*wires) @@ -569,11 +567,9 @@ def test_vector_amplitude(self): reg = regularization.VectorAmplitude(mesh, mapping) np.testing.assert_almost_equal( - reg.objfcts[0].f_m(model.flatten(order='F')), - np.linalg.norm(model, axis=1) + reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) ) - if __name__ == "__main__": unittest.main() From a6efb7580020a73725827b24c5f9869d0ed778e4 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 14:22:29 -0700 Subject: [PATCH 048/455] Re-run black updated --- SimPEG/regularization/vector_amplitude.py | 3 --- tests/base/test_regularization.py | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index ce4e986c37..dace0c84cd 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -102,7 +102,6 @@ def deriv2(self, m, v=None) -> csr_matrix: deriv = 0.0 for f_m_deriv in self.f_m_deriv(m): - if v is None: deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) else: @@ -138,7 +137,6 @@ def f_m(self, m): return np.linalg.norm(self.mapping * self._delta_m(m), axis=0) def f_m_deriv(self, m) -> csr_matrix: - deriv = [] dm = self._delta_m(m) for name, wire in self.mapping.maps: @@ -157,7 +155,6 @@ def f_m(self, m): return self.cell_gradient @ a def f_m_deriv(self, m) -> csr_matrix: - deriv = [] dm = self._delta_m(m) for name, wire in self.mapping.maps: diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 35880a1e73..6c48ffe6ef 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -570,6 +570,7 @@ def test_vector_amplitude(self): reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) ) + def test_WeightedLeastSquares(): mesh = discretize.TensorMesh([3, 4, 5]) @@ -584,5 +585,6 @@ def test_WeightedLeastSquares(): reg.length_scale_z = 0.8 np.testing.assert_allclose(reg.length_scale_z, 0.8) + if __name__ == "__main__": unittest.main() From 466c4c0e75f98f86a88d9a55eafad057097e9d24 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 16:05:20 -0700 Subject: [PATCH 049/455] Flake8 fixes --- SimPEG/regularization/vector_amplitude.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index dace0c84cd..277e08317c 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -1,7 +1,6 @@ from __future__ import annotations import numpy as np -import scipy.sparse as sp from typing import TYPE_CHECKING from discretize.base import BaseMesh @@ -190,21 +189,23 @@ def update_weights(self, m): class VectorAmplitude(Sparse): - """ + r""" The regularization is: The function defined here approximates: .. math:: - \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ - + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ - + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ - + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p \\ + + \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p where $\mathbf{a}(\mathbf{m} - \mathbf{m_{ref})$ is the vector amplitude of the difference between the model and the reference model. .. math:: + \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. From 5b8f3202c0becdc320ed55941d004ab7f35cc84b Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 16:36:57 -0700 Subject: [PATCH 050/455] More flake8 --- SimPEG/regularization/vector_amplitude.py | 2 +- examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py | 1 - examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 277e08317c..75014461cd 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -138,7 +138,7 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] dm = self._delta_m(m) - for name, wire in self.mapping.maps: + for _, wire in self.mapping.maps: deriv += [wire.deriv(dm)] return deriv diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index e0df4671e9..c24b4f6b86 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -16,7 +16,6 @@ """ -from discretize import TreeMesh from SimPEG import ( data, data_misfit, diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 05b150c948..aede5787fd 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -11,7 +11,6 @@ """ from SimPEG import ( - dask, data, data_misfit, directives, @@ -27,7 +26,6 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz from SimPEG.potential_fields import magnetics -import scipy as sp import numpy as np import matplotlib.pyplot as plt From cdff02e32660f1b6c431aacdc64b216b5aa8f539 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 16:43:39 -0700 Subject: [PATCH 051/455] One more --- SimPEG/regularization/vector_amplitude.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 75014461cd..e1620432e2 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -16,7 +16,8 @@ class BaseAmplitude(BaseRegularization): """ - Base vector amplitude function. + Base vector amplitude class. + Requires a mesh and a :obj:`SimPEG.maps.Wires` mapping. """ _W = None @@ -122,10 +123,6 @@ def _nC_residual(self) -> int: class AmplitudeSmallness(SparseSmallness, BaseAmplitude): """ Sparse smallness regularization on vector amplitude. - - **Inputs** - - :param int norm: norm on the smallness """ def f_m(self, m): @@ -145,7 +142,7 @@ def f_m_deriv(self, m) -> csr_matrix: class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): """ - Base Class for sparse regularization on first spatial derivatives + Sparse first spatial derivatives of amplitude. """ def f_m(self, m): @@ -156,7 +153,7 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] dm = self._delta_m(m) - for name, wire in self.mapping.maps: + for _, wire in self.mapping.maps: deriv += [self.cell_gradient * wire.deriv(dm)] return deriv From 8f1d9fe8525e768eb85f6350e2f2e01877c78330 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sat, 8 Apr 2023 22:21:59 -0700 Subject: [PATCH 052/455] Revert changes to sens directives --- SimPEG/directives/directives.py | 4 ---- SimPEG/regularization/vector_amplitude.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 8d23c67cda..2aad10bf2e 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2730,10 +2730,6 @@ def update(self): (reg.mapping * jtj_diag) / reg.regularization_mesh.vol**2.0 ) - if self.normalization: - wr /= wr.max() - - wr += self.threshold wr **= 0.5 # Apply thresholding diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index e1620432e2..86fc4ee1cd 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -206,7 +206,7 @@ class VectorAmplitude(Sparse): \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. - ... + """ def __init__( From bae150ecef6f45a14381afac8a2f29bd62ecf654 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 09:30:50 -0700 Subject: [PATCH 053/455] Add buffer on active cells to stabilize test --- tests/pf/test_pf_quadtree_inversion_linear.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 028966c819..6164e9d968 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -189,9 +189,6 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): grav_srcField = gravity.SourceField([grav_rxLoc]) grav_survey = gravity.Survey(grav_srcField) - # Set only non-zero cells as active - self.active_cells = ~(self.model == 0.0) - # Create the gravity forward model operator self.grav_sim_active = gravity.SimulationEquivalentSourceLayer( self.mesh, @@ -268,7 +265,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): lower=-1.0, upper=1.0, maxIterLS=5, - maxIterCG=10, + maxIterCG=20, tolCG=1e-4, ) @@ -323,9 +320,17 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): 1.0, ) + self.active_cells = utils.model_builder.addBlock( + self.mesh.cell_centers, + np.zeros(self.mesh.nC, dtype=bool), + np.r_[-40, -40], + np.r_[40, 40], + True + ) + # Set only non-zero cells as active. Some tests use all cells # (by not using `self.active_cells`), and others use the active cells - self.active_cells = ~(self.model == 0.0) + # self.active_cells = ~(self.model == 0.0) # Create reduced identity maps. Two versions: for the all-active # and the active-subset models @@ -495,7 +500,7 @@ def test_quadtree_grav_inverse_activecells(self): # Wide difference in results run locally (0.04) versus the pipeline # (0.21), so seems to need unusually large tolerance. print("MODEL RESIDUAL: {}".format(model_residual)) - self.assertAlmostEqual(model_residual, 0.14, delta=0.1) + self.assertAlmostEqual(model_residual, 0.1, delta=0.1) # Check data converged to less than 10% of target misfit data_misfit = 2.0 * self.grav_inv_active.invProb.dmisfit( @@ -509,6 +514,14 @@ def test_quadtree_mag_inverse_activecells(self): # Run the inversion from a zero starting model mrec = self.mag_inv_active.run(np.zeros(int(self.active_cells.sum()))) + import matplotlib.pyplot as plt + fig = plt.figure() + ax = plt.subplot() + m_out = np.zeros(self.mesh.nC) * np.nan + m_out[self.active_cells] = mrec + im = self.mesh.plot_image(m_out, ax=ax) + fig.savefig(r"C:\Users\dominiquef\Desktop\mrec.png") + # Compute predicted data dpred = self.mag_sim_active.dpred(self.mag_model[self.active_cells]) @@ -517,7 +530,7 @@ def test_quadtree_mag_inverse_activecells(self): mrec - self.mag_model[self.active_cells] ) / np.linalg.norm(self.mag_model[self.active_cells]) print("MODEL RESIDUAL: {}".format(model_residual)) - self.assertAlmostEqual(model_residual, 0.11, delta=0.05) + self.assertAlmostEqual(model_residual, 0.01, delta=0.05) # Check data converged to less than 10% of target misfit data_misfit = 2.0 * self.mag_inv_active.invProb.dmisfit( From 799ecdec1b9356b8c7c399f524bed93ca768a066 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 09:49:00 -0700 Subject: [PATCH 054/455] Fix directive, add run test for vector amplitude inversion --- SimPEG/directives/directives.py | 2 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 3 +- tests/pf/test_mag_vector_amplitude.py | 185 ++++++++++++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tests/pf/test_mag_vector_amplitude.py diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 2aad10bf2e..a47a89f38a 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2189,7 +2189,7 @@ def start_irls(self): for obj in reg.objfcts: threshold = np.percentile( - np.abs(obj.f_m(self.invProb.model)), self.prctile + np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile ) if isinstance(obj, SmoothnessFirstOrder): threshold /= reg.regularization_mesh.base_length diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index aede5787fd..a9d733891f 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -155,7 +155,8 @@ wires, active_cells=actv, reference_model_in_smooth=True, - norms=[1.0, 0.0, 0.0, 0.0], + norms=[0.0, 0.0, 0.0, 0.0], + gradient_type="components", ) # Data misfit function diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py new file mode 100644 index 0000000000..f97b2379ea --- /dev/null +++ b/tests/pf/test_mag_vector_amplitude.py @@ -0,0 +1,185 @@ +import unittest +from SimPEG import ( + directives, + maps, + inverse_problem, + optimization, + data_misfit, + inversion, + utils, + regularization, +) + + +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz +import numpy as np +from SimPEG.potential_fields import magnetics as mag +import shutil + + +class MVIProblemTest(unittest.TestCase): + def setUp(self): + np.random.seed(0) + H0 = (50000.0, 90.0, 0.0) + + # The magnetization is set along a different + # direction (induced + remanence) + M = np.array([45.0, 90.0]) + + # Create grid of points for topography + # Lets create a simple Gaussian topo + # and set the active cells + [xx, yy] = np.meshgrid(np.linspace(-200, 200, 50), np.linspace(-200, 200, 50)) + b = 100 + A = 50 + zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0)) + + # We would usually load a topofile + topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + + # Create and array of observation points + xr = np.linspace(-100.0, 100.0, 20) + yr = np.linspace(-100.0, 100.0, 20) + X, Y = np.meshgrid(xr, yr) + Z = A * np.exp(-0.5 * ((X / b) ** 2.0 + (Y / b) ** 2.0)) + 5 + + # Create a MAGsurvey + xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] + rxLoc = mag.Point(xyzLoc) + srcField = mag.SourceField([rxLoc], parameters=H0) + survey = mag.Survey(srcField) + + # Create a mesh + h = [5, 5, 5] + padDist = np.ones((3, 2)) * 100 + + mesh = mesh_builder_xyz( + xyzLoc, h, padding_distance=padDist, depth_core=100, mesh_type="tree" + ) + mesh = refine_tree_xyz( + mesh, topo, method="surface", octree_levels=[4, 4], finalize=True + ) + self.mesh = mesh + # Define an active cells from topo + actv = active_from_xyz(mesh, topo) + nC = int(actv.sum()) + + model = np.zeros((mesh.nC, 3)) + + # Convert the inclination declination to vector in Cartesian + M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) + + # Get the indicies of the magnetized block + ind = utils.model_builder.getIndicesBlock( + np.r_[-20, -20, -10], + np.r_[20, 20, 25], + mesh.gridCC, + )[0] + + # Assign magnetization values + model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + + # Remove air cells + self.model = model[actv, :] + + # Create active map to go from reduce set to full + self.actvMap = maps.InjectActiveCells(mesh, actv, np.nan) + + # Creat reduced identity map + idenMap = maps.IdentityMap(nP=nC * 3) + + # Create the forward model operator + sim = mag.Simulation3DIntegral( + self.mesh, + survey=survey, + model_type="vector", + chiMap=idenMap, + ind_active=actv, + store_sensitivities="disk", + ) + self.sim = sim + + # Compute some data and add some random noise + data = sim.make_synthetic_data( + utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + ) + + wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) + reg = regularization.VectorAmplitude( + mesh, + wires, + active_cells=actv, + reference_model_in_smooth=True, + norms=[0.0, 0.0, 0.0, 0.0], + gradient_type="components", + ) + + # Data misfit function + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + # dmis.W = 1./survey.std + + # Add directives to the inversion + opt = optimization.ProjectedGNCG( + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + ) + + invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) + + # A list of directive to control the inverson + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) + + # Here is where the norms are applied + # Use pick a treshold parameter empirically based on the distribution of + # model parameters + IRLS = directives.Update_IRLS( + f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1 + ) + + # Pre-conditioner + update_Jacobi = directives.UpdatePreconditioner() + sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + self.inv = inversion.BaseInversion( + invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] + ) + + # Run the inversion + self.mstart = np.ones(3 * nC) * 1e-4 # Starting model + + def test_vector_amplitude_inverse(self): + # Run the inversion + mrec = self.inv.run(self.mstart) + + nC = int(mrec.shape[0] / 3) + vec_xyz = mrec.reshape((nC, 3), order="F") + residual = np.linalg.norm(vec_xyz - self.model) / np.linalg.norm(self.model) + + # import matplotlib.pyplot as plt + # ax = plt.subplot() + # self.mesh.plot_slice( + # self.actvMap * mrec.reshape((-1, 3), order="F"), + # v_type="CCv", + # view="vec", + # ax=ax, + # normal="Y", + # grid=True, + # quiver_opts={ + # "pivot": "mid", + # "scale": 8 * np.abs(mrec).max(), + # "scale_units": "inches", + # }, + # ) + # plt.gca().set_aspect("equal", adjustable="box") + # + # plt.show() + + self.assertLess(residual, 1) + # self.assertTrue(residual < 0.05) + + def tearDown(self): + # Clean up the working directory + if self.sim.store_sensitivities == "disk": + shutil.rmtree(self.sim.sensitivity_path) + + +if __name__ == "__main__": + unittest.main() From 8ee45ac3cd11e7856e38741a4ea52c51678af78e Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 09:51:32 -0700 Subject: [PATCH 055/455] Run black --- tests/pf/test_pf_quadtree_inversion_linear.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 6164e9d968..1cd8d57fc4 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -325,7 +325,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): np.zeros(self.mesh.nC, dtype=bool), np.r_[-40, -40], np.r_[40, 40], - True + True, ) # Set only non-zero cells as active. Some tests use all cells @@ -515,6 +515,7 @@ def test_quadtree_mag_inverse_activecells(self): mrec = self.mag_inv_active.run(np.zeros(int(self.active_cells.sum()))) import matplotlib.pyplot as plt + fig = plt.figure() ax = plt.subplot() m_out = np.zeros(self.mesh.nC) * np.nan From 29e482c16b143d1b41ae5a2fbcfba0f96310d4a6 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 10:22:12 -0700 Subject: [PATCH 056/455] FLake8. Comment out plot for test --- tests/pf/test_pf_quadtree_inversion_linear.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 1cd8d57fc4..88f76433e5 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -514,14 +514,14 @@ def test_quadtree_mag_inverse_activecells(self): # Run the inversion from a zero starting model mrec = self.mag_inv_active.run(np.zeros(int(self.active_cells.sum()))) - import matplotlib.pyplot as plt - - fig = plt.figure() - ax = plt.subplot() - m_out = np.zeros(self.mesh.nC) * np.nan - m_out[self.active_cells] = mrec - im = self.mesh.plot_image(m_out, ax=ax) - fig.savefig(r"C:\Users\dominiquef\Desktop\mrec.png") + # import matplotlib.pyplot as plt + # + # fig = plt.figure() + # ax = plt.subplot() + # m_out = np.zeros(self.mesh.nC) * np.nan + # m_out[self.active_cells] = mrec + # self.mesh.plot_image(m_out, ax=ax) + # fig.savefig("mrec.png") # Compute predicted data dpred = self.mag_sim_active.dpred(self.mag_model[self.active_cells]) From e36cc9446f1156ab04f325b76aa6832e0e1b8f9d Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:49:09 -0700 Subject: [PATCH 057/455] Increase coverage --- tests/base/test_regularization.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 6c48ffe6ef..18aeb984a4 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -561,10 +561,19 @@ def test_vector_amplitude(self): with pytest.raises(TypeError, match="A 'mapping' of type"): regularization.VectorAmplitude(mesh, maps.IdentityMap(mesh)) + reg = regularization.VectorAmplitude(mesh) + assert len(reg.objfcts[0].mapping.maps) == 1 + + with pytest.raises(ValueError, match="All models must be the same size!"): + wires = ((f"wire{ind}", mesh.nC+ind) for ind in range(n_comp)) + regularization.VectorAmplitude(mesh, maps.Wires(*wires)) + wires = ((f"wire{ind}", mesh.nC) for ind in range(n_comp)) - mapping = maps.Wires(*wires) - reg = regularization.VectorAmplitude(mesh, mapping) + reg = regularization.VectorAmplitude(mesh, maps.Wires(*wires)) + + with pytest.raises(ValueError, match=f"must be a tuple of len\({n_comp}\)"): + reg.set_weights(abc=(1.0, 1.0)) np.testing.assert_almost_equal( reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) From a27ffe66849e88ebb0dce9a9b97f6e119d31761b Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:50:35 -0700 Subject: [PATCH 058/455] Add parent to BaseRegularizers. Simplify Sparse 'tota' comps, remove diplicate code in VectorAmplitude --- SimPEG/objective_function.py | 4 ++- SimPEG/regularization/base.py | 14 +++++++++ SimPEG/regularization/sparse.py | 31 +++++--------------- SimPEG/regularization/vector_amplitude.py | 35 ++++------------------- 4 files changed, 30 insertions(+), 54 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b63d1bfb71..2ac618541e 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -27,7 +27,7 @@ class BaseObjectiveFunction(BaseSimPEG): mapPair = IdentityMap #: Base class of expected maps _mapping = None #: An IdentityMap instance. _has_fields = False #: should we have the option to store fields - + _parent = None #: parent objective function _nP = None #: number of parameters def __init__(self, nP=None, **kwargs): @@ -265,6 +265,8 @@ def validate_list(objfctlist, multipliers): else: self._nP = fct.nP + fct.parent = self + validate_list(objfcts, multipliers) self.objfcts = objfcts diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 0979c8dcd1..52144e54fb 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -32,6 +32,7 @@ class BaseRegularization(BaseObjectiveFunction): """ _model = None + _parent = None def __init__( self, @@ -137,6 +138,19 @@ def mapping(self, mapping: maps.IdentityMap): ) self._mapping = mapping + @property + def parent(self): + """ + The parent objective function + """ + return self._parent + + @parent.setter + def parent(self, parent): + if not isinstance(parent, ComboObjectiveFunction): + raise TypeError("Parent must be a ComboObjectiveFunction") + self._parent = parent + @property def units(self) -> str | None: """Specify the model units. Special care given to 'radian' values""" diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 127df8dd31..e37bc00749 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -140,30 +140,13 @@ def update_weights(self, m): """ Compute and store the irls weights. """ - if self.gradient_type == "total": - delta_m = self.mapping * self._delta_m(m) - f_m = np.zeros_like(delta_m) - for ii, comp in enumerate("xyz"): - if self.regularization_mesh.dim > ii: - dm = ( - getattr(self.regularization_mesh, f"cell_gradient_{comp}") - * delta_m - ) - - if self.units is not None and self.units.lower() == "radian": - Ave = getattr(self.regularization_mesh, f"aveCC2F{comp}") - length_scales = Ave * ( - self.regularization_mesh.Pac.T - * self.regularization_mesh.mesh.h_gridded[:, ii] - ) - dm = ( - utils.mat_utils.coterminal(dm * length_scales) - / length_scales - ) - - f_m += np.abs( - getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm - ) + if self.gradient_type == "total" and self.parent is not None: + + f_m = np.zeros(self.regularization_mesh.nC) + for obj in self.parent.objfcts: + if isinstance(obj, SparseSmoothness): + avg = getattr(self.regularization_mesh, f"aveF{obj.orientation}2CC") + f_m += np.abs(avg * obj.f_m(m)) f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 86fc4ee1cd..37dddbbb6f 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -67,7 +67,7 @@ def set_weights(self, **weights): if isinstance(values, tuple): if len(values) != len(self.mapping.maps): raise ValueError( - f"Values provided for weight {key} must be of tuple of len({len(self.mapping.maps)})" + f"Values provided for weight {key} must be a tuple of len({len(self.mapping.maps)})" ) for value in values: @@ -158,32 +158,6 @@ def f_m_deriv(self, m) -> csr_matrix: return deriv - def update_weights(self, m): - """ - Compute and store the irls weights. - """ - if self.gradient_type == "total": - delta_m = self.mapping * self._delta_m(m) - delta_m = np.linalg.norm(delta_m, axis=0) - f_m = np.zeros_like(delta_m) - - for ii, comp in enumerate("xyz"): - if self.regularization_mesh.dim > ii: - dm = ( - getattr(self.regularization_mesh, f"cell_gradient_{comp}") - * delta_m - ) - f_m += np.abs( - getattr(self.regularization_mesh, f"aveF{comp}2CC") * dm - ) - - f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m - - else: - f_m = self.f_m(m) - - self.set_weights(irls=self.get_lp_weights(f_m)) - class VectorAmplitude(Sparse): r""" @@ -263,8 +237,11 @@ def mapping(self): return self._mapping @mapping.setter - def mapping(self, wires): - if not isinstance(wires, maps.Wires): + def mapping(self, wires: maps.Wires | None): + if isinstance(wires, type(None)): + wires = maps.Wires(("model", self.regularization_mesh.nC)) + + elif not isinstance(wires, maps.Wires): raise TypeError(f"A 'mapping' of type {maps.Wires} must be provided.") for wire in wires.maps: From 20580bfa5c296afaa2ade7ef266472ba3179a6db Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:50:54 -0700 Subject: [PATCH 059/455] Update example for test --- examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index a9d733891f..a09c9fe99d 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -155,8 +155,8 @@ wires, active_cells=actv, reference_model_in_smooth=True, - norms=[0.0, 0.0, 0.0, 0.0], - gradient_type="components", + norms=[0.0, 2.0, 2.0, 2.0], + gradient_type="total", ) # Data misfit function From 0e2937a97fda813ced7617097d41b124befe8fa8 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:52:01 -0700 Subject: [PATCH 060/455] Black --- SimPEG/regularization/sparse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index e37bc00749..a10cd45e4d 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -141,7 +141,6 @@ def update_weights(self, m): Compute and store the irls weights. """ if self.gradient_type == "total" and self.parent is not None: - f_m = np.zeros(self.regularization_mesh.nC) for obj in self.parent.objfcts: if isinstance(obj, SparseSmoothness): From d87297c7cf4acfb74114b10a6c3ef1ac9f2cf128 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:55:02 -0700 Subject: [PATCH 061/455] re-run blaock on tests --- tests/base/test_regularization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 18aeb984a4..bb68e62e44 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -565,7 +565,7 @@ def test_vector_amplitude(self): assert len(reg.objfcts[0].mapping.maps) == 1 with pytest.raises(ValueError, match="All models must be the same size!"): - wires = ((f"wire{ind}", mesh.nC+ind) for ind in range(n_comp)) + wires = ((f"wire{ind}", mesh.nC + ind) for ind in range(n_comp)) regularization.VectorAmplitude(mesh, maps.Wires(*wires)) wires = ((f"wire{ind}", mesh.nC) for ind in range(n_comp)) From 70825e7b674b07f0320106f79f9493e86126d92a Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 13:59:02 -0700 Subject: [PATCH 062/455] Ignore flake8 on test line --- tests/base/test_regularization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index bb68e62e44..3f88e049bd 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -572,7 +572,7 @@ def test_vector_amplitude(self): reg = regularization.VectorAmplitude(mesh, maps.Wires(*wires)) - with pytest.raises(ValueError, match=f"must be a tuple of len\({n_comp}\)"): + with pytest.raises(ValueError, match=f"must be a tuple of len\({n_comp}\)"): # noqa: W605 reg.set_weights(abc=(1.0, 1.0)) np.testing.assert_almost_equal( From 43acad10662967d1722f2abfc90333627d0743d9 Mon Sep 17 00:00:00 2001 From: fourndo Date: Sun, 9 Apr 2023 14:01:19 -0700 Subject: [PATCH 063/455] ONe time --- tests/base/test_regularization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 3f88e049bd..f2fa2e1032 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -572,7 +572,9 @@ def test_vector_amplitude(self): reg = regularization.VectorAmplitude(mesh, maps.Wires(*wires)) - with pytest.raises(ValueError, match=f"must be a tuple of len\({n_comp}\)"): # noqa: W605 + with pytest.raises( + ValueError, match=f"must be a tuple of len\({n_comp}\)" # noqa: W605 + ): reg.set_weights(abc=(1.0, 1.0)) np.testing.assert_almost_equal( From f10330f8d6b09e78a6dc05e824008deba0fc0795 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 14 Apr 2023 15:40:24 -0700 Subject: [PATCH 064/455] update returned type --- SimPEG/regularization/vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 5eca871765..15f0c66c01 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -11,7 +11,7 @@ class BaseVectorRegularization(BaseRegularization): """The regularizers work on models where each value is a vector.""" @property - def _weights_shapes(self) -> tuple[int] | str: + def _weights_shapes(self) -> list[tuple[int]]: """Acceptable lengths for the weights Returns From f46bbe8d9b9946e4afdd46937d32a52ffbc27320 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 18 Apr 2023 16:20:45 -0700 Subject: [PATCH 065/455] add a jtj_diag method for multiprocessing --- SimPEG/meta/multiprocessing.py | 34 ++++++++++++++++++++++++++++++---- SimPEG/meta/simulation.py | 5 ++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index e56a046a2f..3b18fe8f1c 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -98,9 +98,9 @@ def run(self): r_queue.put(sim.Jtvec(sim.model, v, fields)) elif op == 5: # do jtj_diag - f_key = args + w, f_key = args fields = _cached_items[f_key] - r_queue.put(sim.getJtJdiag(sim.model, fields)) + r_queue.put(sim.getJtJdiag(sim.model, w, fields)) def store_model(self, m): self._check_closed() @@ -125,9 +125,17 @@ def start_jt_vec(self, v, f_future): self._check_closed() self.task_queue.put((4, (v, f_future.item_id))) - def start_jtj_diag(self, f_future): + def start_jtj_diag(self, w, f_future): self._check_closed() - self.task_queue.put((5, (f_future.item_id,))) + self.task_queue.put( + ( + 5, + ( + w, + f_future.item_id, + ), + ) + ) def result(self): self._check_closed() @@ -240,6 +248,24 @@ def Jtvec(self, m, v, f=None): jt_vec += p.result() return jt_vec + def getJtJdiag(self, m, v, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + chunk_w = W[self._data_offsets[i] : self._data_offsets[i + 1]] + p.start_jtj_diag(chunk_w, field) + jtj_diag = 0.0 + for p in self._sim_processes: + jtj_diag += p.result() + self._jtjdiag = jtj_diag + return self._jtjdiag + def close(self): for p in self._sim_processes: if p.is_alive(): diff --git a/SimPEG/meta/simulation.py b/SimPEG/meta/simulation.py index 17448a7e5c..900bacac85 100644 --- a/SimPEG/meta/simulation.py +++ b/SimPEG/meta/simulation.py @@ -284,7 +284,10 @@ def getJtJdiag(self, m, W=None, f=None): if W is None: W = np.ones(self.survey.nD) else: - W = W.diagonal() + try: + W = W.diagonal() + except (AttributeError, TypeError, ValueError): + pass jtj_diag = 0.0 # approximate the JtJ diag on the full model space as: # sum((diag(sqrt(jtj_diag)) @ M_deriv))**2) From 7f0c7a5d14ccd305769f1115ddb3d7a2b0a9019a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 20 Apr 2023 12:35:01 -0700 Subject: [PATCH 066/455] unconditionally reshape the output of mapped jtj_diag --- SimPEG/directives/directives.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 648115fbf7..8d84292474 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2707,15 +2707,15 @@ def update(self): for reg in self.reg.objfcts: if not isinstance(reg, BaseSimilarityMeasure): mesh = reg.regularization_mesh + n_cells = mesh.nC + mapped_jtj_diag = reg.mapping * jtj_diag - if mapped_jtj_diag.size // mesh.dim > 1: - mapped_jtj_diag = mapped_jtj_diag.reshape((mesh.nC, -1), order="F") - wr_temp = ( - mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 - ) - wr_temp = wr_temp.reshape(-1, order="F") - else: - wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol**2 + # reshape the mapped, so you can divide by volume + # (let's say it was a vector or anisotropic model) + mapped_jtj_diag = mapped_jtj_diag.reshape((n_cells, -1), order="F") + wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 + wr_temp = wr_temp.reshape(-1, order="F") + wr += reg.mapping.deriv(self.invProb.model).T * wr_temp wr **= 0.5 From 98c81bc03b95bcc05097db6bdf60c30049570b57 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 20 Apr 2023 12:40:35 -0700 Subject: [PATCH 067/455] remove unused import --- SimPEG/directives/directives.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 8d84292474..405039ff48 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -18,7 +18,6 @@ SmoothnessFirstOrder, SparseSmoothness, BaseSimilarityMeasure, - BaseVectorRegularization, ) from ..utils import ( mkvc, From 1653cf9315bc572093c121b63bdf29635a0dc181 Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 20 Apr 2023 13:20:29 -0700 Subject: [PATCH 068/455] Pass m to derivs --- SimPEG/regularization/vector_amplitude.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py index 37dddbbb6f..dd24754acc 100644 --- a/SimPEG/regularization/vector_amplitude.py +++ b/SimPEG/regularization/vector_amplitude.py @@ -91,7 +91,7 @@ def deriv(self, m) -> np.ndarray: deriv = 0.0 - for f_m_deriv in self.f_m_deriv(d_m): + for f_m_deriv in self.f_m_deriv(m): deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * d_m) return deriv @@ -134,9 +134,8 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] - dm = self._delta_m(m) for _, wire in self.mapping.maps: - deriv += [wire.deriv(dm)] + deriv += [wire.deriv(m)] return deriv @@ -152,9 +151,8 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: deriv = [] - dm = self._delta_m(m) for _, wire in self.mapping.maps: - deriv += [self.cell_gradient * wire.deriv(dm)] + deriv += [self.cell_gradient * wire.deriv(m)] return deriv From f2b58d4075c1a9b0e40927f9c7a759111cfbadbf Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 24 Apr 2023 08:15:11 -0700 Subject: [PATCH 069/455] Migrate amplitude vector to Joe's base class --- SimPEG/regularization/__init__.py | 11 +- SimPEG/regularization/base.py | 1 + SimPEG/regularization/vector.py | 312 ++++++++++++++++++ SimPEG/regularization/vector_amplitude.py | 253 -------------- .../plot_inv_mag_MVI_VectorAmplitude.py | 2 +- 5 files changed, 324 insertions(+), 255 deletions(-) create mode 100644 SimPEG/regularization/vector.py delete mode 100644 SimPEG/regularization/vector_amplitude.py diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index c609e99cd1..9333d14bb7 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -158,6 +158,14 @@ SparseSmallness SparseSmoothness +Vector Regularizations +---------------------- +The regularizations are meant for models of vectors. + +.. autosummary:: + :toctree: generated/ + CrossReferenceRegularization + Joint Regularizations --------------------- There are several joint inversion regularizers available @@ -180,6 +188,7 @@ BaseRegularization BaseSimilarityMeasure BaseSparse + BaseVectorRegularization """ from ..utils.code_utils import deprecate_class @@ -197,7 +206,7 @@ from .cross_gradient import CrossGradient from .correspondence import LinearCorrespondence from .jtv import JointTotalVariation -from .vector_amplitude import VectorAmplitude +from .vector import BaseVectorRegularization, CrossReferenceRegularization, VectorAmplitude @deprecate_class(removal_version="0.19.0", future_warn=True) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 52144e54fb..834bca1b04 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -33,6 +33,7 @@ class BaseRegularization(BaseObjectiveFunction): _model = None _parent = None + _W = None def __init__( self, diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py new file mode 100644 index 0000000000..67735019b8 --- /dev/null +++ b/SimPEG/regularization/vector.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import scipy.sparse as sp +import numpy as np +from .base import Smallness +from discretize.base import BaseMesh +from .base import RegularizationMesh, BaseRegularization +from .sparse import Sparse, SparseSmallness, SparseSmoothness + +if TYPE_CHECKING: + from scipy.sparse import csr_matrix + +# Regularizations for vector models. + + +class BaseVectorRegularization(BaseRegularization): + """The regularizers work on models where each value is a vector.""" + + @property + def n_comp(self): + """Number of components in the model.""" + return int(self.mapping.shape[0] / self.regularization_mesh.nC) + + @property + def _weights_shapes(self) -> list[tuple[int]]: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + mesh = self.regularization_mesh + return [(mesh.nC,), (self.n_comp * mesh.nC,), (mesh.nC, self.n_comp)] + + +class CrossReferenceRegularization(Smallness, BaseVectorRegularization): + def __init__( + self, mesh, ref_dir, active_cells=None, mapping=None, weights=None, **kwargs + ): + kwargs.pop("reference_model", None) + super().__init__( + mesh=mesh, + active_cells=active_cells, + mapping=mapping, + weights=weights, + **kwargs, + ) + self.ref_dir = ref_dir + self.reference_model = 0.0 + + @property + def _nC_residual(self): + return np.prod(self.ref_dir.shape) + + @property + def ref_dir(self): + return self._ref_dir + + @ref_dir.setter + def ref_dir(self, value): + mesh = self.regularization_mesh + nC = mesh.nC + value = np.asarray(value) + if value.shape != (nC, self.n_comp): + if value.shape == (self.n_comp,): + # expand it out for each mesh cell + value = np.tile(value, (nC, 1)) + else: + raise ValueError(f"ref_dir must be shape {(nC, self.n_comp)}") + self._ref_dir = value + + R0 = sp.diags(value[:, 0]) + R1 = sp.diags(value[:, 1]) + if value.shape[1] == 2: + X = sp.bmat([[R1, -R0]]) + elif value.shape[1] == 3: + Z = sp.csr_matrix((nC, nC)) + R2 = sp.diags(value[:, 2]) + X = sp.bmat( + [ + [Z, R2, -R1], + [-R2, Z, R0], + [R1, -R0, Z], + ] + ) + self._X = X + + def f_m(self, m): + return self._X @ (self.mapping * m) + + def f_m_deriv(self, m): + return self._X @ self.mapping.deriv(m) + + @property + def W(self): + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh + nC = mesh.nC + + weights = np.ones( + nC, + ) + for value in self._weights.values(): + if value.shape == (nC,): + weights *= value + elif value.size == (self.n_comp * nC,): + weights *= np.linalg.norm( + value.reshape((nC, self.n_comp), order="F"), axis=1 + ) + weights = np.sqrt(weights) + self._W = sp.diags(np.r_[weights, weights, weights], format="csr") + return self._W + + +class BaseAmplitude(BaseVectorRegularization): + """ + Base vector amplitude class. + Requires a mesh and a :obj:`SimPEG.maps.Wires` mapping. + """ + def amplitude(self, m): + return np.linalg.norm( + ( + self.mapping * self._delta_m(m) + ).reshape( + (self.regularization_mesh.nC, self.n_comp), order="F" + ), axis=1 + ) + + def deriv(self, m) -> np.ndarray: + """ """ + d_m = self._delta_m(m) + + return self.f_m_deriv(m).T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * self.f_m_deriv(m) * d_m) + + def deriv2(self, m, v=None) -> csr_matrix: + """ """ + f_m_deriv = self.f_m_deriv(m) + + if v is None: + return f_m_deriv.T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv) + + return f_m_deriv.T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv * v) + + +class AmplitudeSmallness(SparseSmallness, BaseAmplitude): + """ + Sparse smallness regularization on vector amplitude. + """ + + def f_m(self, m): + """ + Compute the amplitude of a vector model. + """ + + return self.amplitude(m) + + @property + def W(self): + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh + nC = mesh.nC + + weights = np.ones( + nC, + ) + for value in self._weights.values(): + if value.shape == (nC,): + weights *= value + elif value.shape == (self.n_comp * nC,): + weights *= np.linalg.norm( + value.reshape((nC, self.n_comp), order="F"), axis=1 + ) + + self._W = sp.diags(np.sqrt(weights), format="csr") + + return self._W + + +class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): + """ + Sparse first spatial derivatives of amplitude. + """ + + @property + def _weights_shapes(self) -> list[tuple[int]]: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + nC = self.regularization_mesh.nC + nF = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ).shape[0] + return [(nF,), (self.n_comp * nF,), (nF, self.n_comp), (nC,), (self.n_comp * nC,), (nC, self.n_comp)] + + def f_m(self, m): + a = self.amplitude(m) + + return self.cell_gradient @ a + + def f_m_deriv(self, m) -> csr_matrix: + """""" + return sp.block_diag([self.cell_gradient] * self.n_comp) @ self.mapping.deriv(self._delta_m(m)) + + @property + def W(self): + """ + Weighting matrix that takes the volumes, free weights, fixed weights and + length scales of the difference operator (normalized optional). + """ + if getattr(self, "_W", None) is None: + average_cell_2_face = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ) + nC = self.regularization_mesh.nC + nF = average_cell_2_face.shape[0] + weights = 1.0 + for values in self._weights.values(): + if values.shape[0] == nC: + values = average_cell_2_face * values + elif not values.shape == (nF,): + values = np.linalg.norm( + values.reshape((-1, self.n_comp), order="F"), axis=1 + ) + if values.size == nC: + values = average_cell_2_face * values + + weights *= values + + self._W = sp.diags(np.sqrt(weights), format="csr") + + return self._W + + +class VectorAmplitude(Sparse): + r""" + The regularization is: + + The function defined here approximates: + + .. math:: + + \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + + where $\mathbf{a}(\mathbf{m} - \mathbf{m_{ref})$ is the vector amplitude of the difference between + the model and the reference model. + + .. math:: + + \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} + + where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. + + """ + + def __init__( + self, + mesh, + mapping=None, + active_cells=None, + **kwargs, + ): + if not isinstance(mesh, (BaseMesh, RegularizationMesh)): + raise TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + f"Value of type {type(mesh)} provided." + ) + + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + + self._regularization_mesh = mesh + + if active_cells is not None: + self._regularization_mesh.active_cells = active_cells + + objfcts = [ + AmplitudeSmallness(mesh=self.regularization_mesh, mapping=mapping), + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="x", mapping=mapping + ), + ] + + if mesh.dim > 1: + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="y", mapping=mapping + ) + ) + + if mesh.dim > 2: + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="z", mapping=mapping + ) + ) + + super().__init__( + self.regularization_mesh, + objfcts=objfcts, + mapping=mapping, + **kwargs, + ) diff --git a/SimPEG/regularization/vector_amplitude.py b/SimPEG/regularization/vector_amplitude.py deleted file mode 100644 index dd24754acc..0000000000 --- a/SimPEG/regularization/vector_amplitude.py +++ /dev/null @@ -1,253 +0,0 @@ -from __future__ import annotations - -import numpy as np -from typing import TYPE_CHECKING - -from discretize.base import BaseMesh -from SimPEG import maps -from .base import RegularizationMesh, BaseRegularization -from .sparse import Sparse, SparseSmallness, SparseSmoothness -from .. import utils -from SimPEG.utils.code_utils import validate_ndarray_with_shape - -if TYPE_CHECKING: - from scipy.sparse import csr_matrix - - -class BaseAmplitude(BaseRegularization): - """ - Base vector amplitude class. - Requires a mesh and a :obj:`SimPEG.maps.Wires` mapping. - """ - - _W = None - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - - @property - def mapping(self) -> maps.Wires: - return self._mapping - - @mapping.setter - def mapping(self, wires: maps.Wires | None): - if isinstance(wires, type(None)): - wires = maps.Wires(("model", self.regularization_mesh.nC)) - - elif not isinstance(wires, maps.Wires): - raise TypeError(f"A 'mapping' of type {maps.Wires} must be provided.") - - for wire in wires.maps: - if wire[1].shape[0] != self.regularization_mesh.nC: - raise ValueError( - f"All models must be the same size! Got {wire} with shape{wire[1].shape[0]}" - ) - self._mapping = wires - - def set_weights(self, **weights): - """Adds (or updates) the specified weights to the regularization - - Parameters: - ----------- - **kwargs : key, numpy.ndarray - Each keyword argument is added to the weights used by the regularization. - They can be accessed with their keyword argument. - - Examples - -------- - >>> import discretize - >>> from SimPEG.regularization import Smallness - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = Smallness(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - """ - for key, values in weights.items(): - if isinstance(values, tuple): - if len(values) != len(self.mapping.maps): - raise ValueError( - f"Values provided for weight {key} must be a tuple of len({len(self.mapping.maps)})" - ) - - for value in values: - validate_ndarray_with_shape( - "weights", value, shape=self._weights_shapes, dtype=float - ) - - self._weights[key] = np.linalg.norm(np.vstack(values), axis=0) - else: - validate_ndarray_with_shape( - "weights", values, shape=self._weights_shapes, dtype=float - ) - self._weights[key] = values - - self._W = None - - @utils.timeIt - def deriv(self, m) -> np.ndarray: - """ """ - d_m = self._delta_m(m) - - deriv = 0.0 - - for f_m_deriv in self.f_m_deriv(m): - deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * d_m) - - return deriv - - @utils.timeIt - def deriv2(self, m, v=None) -> csr_matrix: - """ """ - deriv = 0.0 - - for f_m_deriv in self.f_m_deriv(m): - if v is None: - deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) - else: - deriv += f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv * v) - - return deriv - - @property - def _nC_residual(self) -> int: - """ - Shape of the residual - """ - if self.mapping is None: - raise AttributeError("The regularization does not have a 'mapping' yet.") - - return int(np.sum([wire.shape[0] for (_, wire) in self.mapping.maps])) - - -class AmplitudeSmallness(SparseSmallness, BaseAmplitude): - """ - Sparse smallness regularization on vector amplitude. - """ - - def f_m(self, m): - """ - Compute the amplitude of a vector model. - """ - - return np.linalg.norm(self.mapping * self._delta_m(m), axis=0) - - def f_m_deriv(self, m) -> csr_matrix: - deriv = [] - for _, wire in self.mapping.maps: - deriv += [wire.deriv(m)] - return deriv - - -class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): - """ - Sparse first spatial derivatives of amplitude. - """ - - def f_m(self, m): - a = np.linalg.norm(self.mapping * self._delta_m(m), axis=0) - - return self.cell_gradient @ a - - def f_m_deriv(self, m) -> csr_matrix: - deriv = [] - for _, wire in self.mapping.maps: - deriv += [self.cell_gradient * wire.deriv(m)] - - return deriv - - -class VectorAmplitude(Sparse): - r""" - The regularization is: - - The function defined here approximates: - - .. math:: - - \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - - where $\mathbf{a}(\mathbf{m} - \mathbf{m_{ref})$ is the vector amplitude of the difference between - the model and the reference model. - - .. math:: - - \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} - - where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. - - """ - - def __init__( - self, - mesh, - mapping=None, - active_cells=None, - **kwargs, - ): - if not isinstance(mesh, (BaseMesh, RegularizationMesh)): - raise TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - - if not isinstance(mesh, RegularizationMesh): - mesh = RegularizationMesh(mesh) - - self._regularization_mesh = mesh - - if active_cells is not None: - self._regularization_mesh.active_cells = active_cells - - objfcts = [ - AmplitudeSmallness(mesh=self.regularization_mesh, mapping=mapping), - AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="x", mapping=mapping - ), - ] - - if mesh.dim > 1: - objfcts.append( - AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="y", mapping=mapping - ) - ) - - if mesh.dim > 2: - objfcts.append( - AmplitudeSmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="z", mapping=mapping - ) - ) - - super().__init__( - self.regularization_mesh, - objfcts=objfcts, - mapping=mapping, - **kwargs, - ) - - @property - def mapping(self): - return self._mapping - - @mapping.setter - def mapping(self, wires: maps.Wires | None): - if isinstance(wires, type(None)): - wires = maps.Wires(("model", self.regularization_mesh.nC)) - - elif not isinstance(wires, maps.Wires): - raise TypeError(f"A 'mapping' of type {maps.Wires} must be provided.") - - for wire in wires.maps: - if wire[1].shape[0] != self.regularization_mesh.nC: - raise ValueError( - f"All models must be the same size! Got {wire} with shape{wire[1].shape[0]}" - ) - self._mapping = wires - - for fct in self.objfcts: - fct.mapping = wires diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index a09c9fe99d..1479020d99 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -152,7 +152,7 @@ # Create the regularization on the amplitude of magnetization reg = regularization.VectorAmplitude( mesh, - wires, + mapping=idenMap, active_cells=actv, reference_model_in_smooth=True, norms=[0.0, 2.0, 2.0, 2.0], From bcd83ce10c30d3018d119a4d6f1c7c6015ac5957 Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 24 Apr 2023 08:16:23 -0700 Subject: [PATCH 070/455] Run black --- SimPEG/directives/directives.py | 1 - SimPEG/regularization/__init__.py | 6 +++++- SimPEG/regularization/vector.py | 33 ++++++++++++++++++++++--------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index c21643315f..96609e9a13 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2717,7 +2717,6 @@ def update(self): # Compute and sum root-mean squared sensitivities for all objective functions wr = np.zeros_like(self.invProb.model) for reg in self.reg.objfcts: - if isinstance(reg, BaseSimilarityMeasure): continue diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 9333d14bb7..33327b3d7f 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -206,7 +206,11 @@ from .cross_gradient import CrossGradient from .correspondence import LinearCorrespondence from .jtv import JointTotalVariation -from .vector import BaseVectorRegularization, CrossReferenceRegularization, VectorAmplitude +from .vector import ( + BaseVectorRegularization, + CrossReferenceRegularization, + VectorAmplitude, +) @deprecate_class(removal_version="0.19.0", future_warn=True) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 67735019b8..d56e6052a1 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -120,29 +120,35 @@ class BaseAmplitude(BaseVectorRegularization): Base vector amplitude class. Requires a mesh and a :obj:`SimPEG.maps.Wires` mapping. """ + def amplitude(self, m): return np.linalg.norm( - ( - self.mapping * self._delta_m(m) - ).reshape( + (self.mapping * self._delta_m(m)).reshape( (self.regularization_mesh.nC, self.n_comp), order="F" - ), axis=1 + ), + axis=1, ) def deriv(self, m) -> np.ndarray: """ """ d_m = self._delta_m(m) - return self.f_m_deriv(m).T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * self.f_m_deriv(m) * d_m) + return self.f_m_deriv(m).T * ( + sp.block_diag([self.W.T * self.W] * self.n_comp) * self.f_m_deriv(m) * d_m + ) def deriv2(self, m, v=None) -> csr_matrix: """ """ f_m_deriv = self.f_m_deriv(m) if v is None: - return f_m_deriv.T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv) + return f_m_deriv.T * ( + sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv + ) - return f_m_deriv.T * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv * v) + return f_m_deriv.T * ( + sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv * v + ) class AmplitudeSmallness(SparseSmallness, BaseAmplitude): @@ -197,7 +203,14 @@ def _weights_shapes(self) -> list[tuple[int]]: nF = getattr( self.regularization_mesh, "aveCC2F{}".format(self.orientation) ).shape[0] - return [(nF,), (self.n_comp * nF,), (nF, self.n_comp), (nC,), (self.n_comp * nC,), (nC, self.n_comp)] + return [ + (nF,), + (self.n_comp * nF,), + (nF, self.n_comp), + (nC,), + (self.n_comp * nC,), + (nC, self.n_comp), + ] def f_m(self, m): a = self.amplitude(m) @@ -206,7 +219,9 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: """""" - return sp.block_diag([self.cell_gradient] * self.n_comp) @ self.mapping.deriv(self._delta_m(m)) + return sp.block_diag([self.cell_gradient] * self.n_comp) @ self.mapping.deriv( + self._delta_m(m) + ) @property def W(self): From 0bb5dad49fc638003e2cc0ecd396c5996ac83c8c Mon Sep 17 00:00:00 2001 From: fourndo Date: Mon, 24 Apr 2023 08:28:50 -0700 Subject: [PATCH 071/455] Update test --- tests/pf/test_mag_vector_amplitude.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index f97b2379ea..bfd94afc13 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -104,10 +104,9 @@ def setUp(self): utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True ) - wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) reg = regularization.VectorAmplitude( mesh, - wires, + mapping=idenMap, active_cells=actv, reference_model_in_smooth=True, norms=[0.0, 0.0, 0.0, 0.0], From b4bc5922b19c70c83089535190dda34d32efb3b2 Mon Sep 17 00:00:00 2001 From: dccowan Date: Thu, 4 May 2023 11:06:16 -0700 Subject: [PATCH 072/455] update regularization docstrings --- SimPEG/regularization/__init__.py | 185 ++++++---------- SimPEG/regularization/base.py | 341 +++++++++++++++++++++--------- 2 files changed, 306 insertions(+), 220 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index c411dd3d00..99308fa187 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -2,154 +2,93 @@ ============================================= Regularization (:mod:`SimPEG.regularization`) ============================================= -.. currentmodule:: SimPEG.regularization - -If there is one model that has a misfit that equals the desired tolerance, -then there are infinitely many other models which can fit to the same degree. -The challenge is to find that model which has the desired characteristics and -is compatible with a priori information. A single model can be selected from -an infinite ensemble by measuring the length, or norm, of each model. Then a -smallest, or sometimes largest, member can be isolated. Our goal is to design -a norm that embodies our prior knowledge and, when minimized, yields a -realistic candidate for the solution of our problem. The norm can penalize -variation from a reference model, spatial derivatives of the model, or some -combination of these. - -WeightedLeastSquares Regularization -=================================== - -Here we will define regularization of a model, m, in general however, this -should be thought of as (m-m_ref) but otherwise it is exactly the same: - -.. math:: - - R(m) = - \int_\Omega - \frac{\alpha_x}{2} - \left( \frac{\partial m}{\partial x} \right)^2 - + \frac{\alpha_y}{2} - \left( \frac{\partial m}{\partial y} \right)^2 - \partial v - -Our discrete gradient operator works on cell centers and gives the derivative -on the cell faces, which is not where we want to be evaluating this integral. -We need to average the values back to the cell-centers before we integrate. To -avoid null spaces, we square first and then average. In 2D with ij notation it -looks like this: - -.. math:: - - R(m) \approx - \sum_{ij} - \left[ - \frac{\alpha_x}{2} \left[ - \left( \frac{m_{i+1,j} - m_{i,j}}{h} \right)^2 - + \left( \frac{m_{i,j} - m_{i-1,j}}{h} \right)^2 - \right] \\ - + \frac{\alpha_y}{2} \left[ - \left( \frac{m_{i,j+1} - m_{i,j}}{h} \right)^2 - + \left( \frac{m_{i,j} - m_{i,j-1}}{h} \right)^2 - \right] - \right] h^2 - -If we let D_1 be the derivative matrix in the x direction - -.. math:: - - \mathbf{D}_1 = \mathbf{I}_2\otimes\mathbf{d}_1 - -.. math:: - - \mathbf{D}_2 = \mathbf{d}_2\otimes\mathbf{I}_1 - -Where d_1 is the one dimensional derivative: -.. math:: - - \mathbf{d}_1 = \frac{1}{h} \left[ \begin{array}{cccc} - -1 & 1 & & \\ - & \ddots & \ddots&\\ - & & -1 & 1\end{array} \right] - -.. math:: - - R(m) \approx - \mathbf{v}^\top \left[ - \frac{\alpha_x}{2} \mathbf{A}_1 (\mathbf{D}_1 m) \odot (\mathbf{D}_1 m) - + \frac{\alpha_y}{2} \mathbf{A}_2 (\mathbf{D}_2 m) \odot (\mathbf{D}_2 m) - \right] - -Recall that this is really a just point wise multiplication, or a diagonal -matrix times a vector. When we multiply by something in a diagonal we can -interchange and it gives the same results (i.e. it is point wise) - -.. math:: - - \mathbf{a\odot b} - = \text{diag}(\mathbf{a})\mathbf{b} - = \text{diag}(\mathbf{b})\mathbf{a} - = \mathbf{b\odot a} +.. currentmodule:: SimPEG.regularization -and the transpose also is true (but the sizes have to make sense...): +``Regularization`` classes are used to impose constraints on models recovered through geophysical inversion. +Constraints may be straight forward, such as: setting upper and lower bounds for values in the recovered model, +requiring the recovered model be spatially smooth, or using a reference model to add a-priori information. +Constraints may also be more sophisticated; e.g. cross-validation and petrophysically-guided regularization. +In SimPEG, constraints on the recovered model can be defined using a single ``Regularization`` object, +or defined by combining multiple ``Regularization`` objects. -.. math:: +Basic Theory +------------ - \mathbf{a}^\top\text{diag}(\mathbf{b}) = \mathbf{b}^\top\text{diag}(\mathbf{a}) +Most geophysical inverse problems suffer from non-uniqueness; i.e. there is an infinite number of models +(:math:`m`) capable of reproducing the observed data to within a specified degree of uncertainty. +The challenge is recovering a model which 1) reproduces the observed data, and 2) reasonably approximates +the subsurface structures responsible for the observed geophysical response. To accomplish this, +regularization functions are used to ensure the solution to the inverse problem is unique and is +geologically plausible. The choice in regularization function(s) depends on user assumptions and +a-prior information. -So R(m) can simplify to: +SimPEG uses a deterministic inversion approach to recover an appropriate model. The algorithm does this +by finding the model (:math:`m`) which minimizes a global objective function (or penalty function) of the form: .. math:: + \phi (m) = \phi_d (m) + \beta \, \phi_m (m) - R(m) \approx - \mathbf{m}^\top \left[ - \frac{\alpha_x}{2} \mathbf{D}_1^\top - \text{diag} (\mathbf{A}_1^\top \mathbf{v}) \mathbf{D}_1 - + \frac{\alpha_y}{2} \mathbf{D}_2^\top - \text{diag} (\mathbf{A}_2^\top \mathbf{v}) \mathbf{D}_2 - \right] - \mathbf{m} - -We will define W_x as: +The global objective function contains two terms: a data misfit term :math:`\phi_d` which +ensures data predicted by the recovered model adequately reproduces the observed data, and the model +objective function :math:`\phi_m` which is comprised of one or more regularization functions. I.e.: .. math:: + \phi_m (m) = \sum_i \alpha_i \, r_i (m) - \mathbf{W}_x = - \sqrt{\alpha_x} - \text{diag} \left(\sqrt{\mathbf{A}_1^\top\mathbf{v}}\right) \mathbf{D}_1 +The model objective function imposes all of the desired constraints on the recovered model. +Constants :math:`\alpha_i` weight the relative contributions of the regularization functions +comprising the model objective function. The trade-off parameter :math:`\beta` balances the +relative contribution of the data misfit and regularization functions on the global objective function. - -And then W as a tall matrix of all of the different regularization terms: +Regularization classes within SimPEG correspond to different regularization functions that can be +used individually or combined to define the model objective function :math:`\phi_m (\mathbf{m})`. +For example, a combination of regularization functions that ensures the values in the recovered +model are not too large and are spatially smooth in the x and y-directions can be expressed as: .. math:: + \phi_m (m) = \int_\Omega \, \bigg [ \, + \frac{\alpha_s}{2} \, m^2 + + \frac{\alpha_x}{2} \bigg ( \frac{\partial m}{\partial x} \bigg )^2 + + \frac{\alpha_y}{2} \bigg ( \frac{\partial m}{\partial y} \bigg )^2 + \bigg ] \, d v - \mathbf{W} = \left[ \begin{array}{c} - \mathbf{W}_s\\ - \mathbf{W}_x\\ - \mathbf{W}_y\end{array} \right] - -Then we can write +Discretized to a numerical grid (or mesh), the model becomes a discrete vector :math:`\mathbf{m}`. +And the aforementioned expression is approximately by: .. math:: + \phi_m (\mathbf{m}) & \approx \frac{\alpha_s}{2} \mathbf{m^T V^T V m} + + \frac{\alpha_x}{2} \mathbf{m^T V^T G_x^T G_x V m} + + \frac{\alpha_y}{2} \mathbf{m^T V^T G_y^T G_y V m} + = \frac{1}{2} \mathbf{m^T R^T R m} - R(m) \approx \frac{1}{2}\mathbf{m^\top W^\top W m} +where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradients along the x and y-directions +respectively, and :math:`\mathbf{V}` is a diagonal matrix containing the square-roots of the cell +volumes. As is the case with multiple least-squares regularization functions, the terms can be amalgamated +and used to define the model objective function using a single regularization operator :math:`\mathbf{R}`. The API ======= -Least Squares Regularizations ------------------------------ +Weighted Least Squares Regularization +------------------------------------- +Weighted least squares regularization functions are defined as weighted L2-norms on the model, its first-order +directional derivative(s), or its second-order direction derivative(s). + .. autosummary:: :toctree: generated/ - WeightedLeastSquares Smallness SmoothnessFirstOrder SmoothnessSecondOrder + WeightedLeastSquares -Sparse Regularizations ----------------------- -We have also implemented several sparse regularizations with a variable norm. +Sparse Norm Regularization +-------------------------- +Sparse norm regularization functions allow for the recovery of compact and/or blocky structures. +An iteratively re-weighted least-squares approach allows smallness and smoothness +regularization functions to be defined using norms between 0 and 2. .. autosummary:: :toctree: generated/ @@ -158,9 +97,9 @@ SparseSmallness SparseSmoothness -Joint Regularizations ---------------------- -There are several joint inversion regularizers available +Joint Regularization +-------------------- +Regularization functions for joint inversion involving one or more physical properties. .. autosummary:: :toctree: generated/ @@ -171,8 +110,10 @@ PGIsmallness LinearCorrespondence -Base Regularization classes +Base Regularization Classes --------------------------- +Base regularization classes. Inherited by other classes and not used directly to constrain inversions. + .. autosummary:: :toctree: generated/ diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 263fc64f8e..034e21a0b3 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -16,19 +16,28 @@ class BaseRegularization(BaseObjectiveFunction): - """ - Base class for regularization. Inherit this for building your own - regularization. The base regularization assumes a weighted l2-norm style of - regularization. However, if you wish to employ a different norm, the - methods :meth:`__call__`, :meth:`deriv` and :meth:`deriv2` can be - over-written - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param active_cells: Array of bool defining the set of active cells. - :param mapping: Model map - :param reference_model: Array of model values used to constrain the inversion - :param units: Model units identifier. Special case for 'radian' - :param weights: Weight multipliers to customize the least-squares function. + """Base class for least-squares regularization. + + The ``BaseRegularization`` class defines properties and methods inherited by least-squares + regularization classes. It is not directly used to constrain the inversions. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of regularization mesh cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the regularization mesh. + + """ _model = None @@ -72,7 +81,10 @@ def __init__( @property def active_cells(self) -> np.ndarray: - """A boolean array of active cells on the regularization + """A boolean array of active cells, defined on the regularization mesh. + + Defines the cells in the regularization mesh that are active throughout the inversion. + Inactive cells remain fixed and are defined according to the starting model. Returns ------- @@ -81,7 +93,8 @@ def active_cells(self) -> np.ndarray: Notes ----- If this is set with an array of integers, it interprets it as an array - listing the active cell indices. + listing the active cell indices. When called, the quantity will have + been converted to a boolean array. """ return self.regularization_mesh.active_cells @@ -107,7 +120,13 @@ def active_cells(self, values: np.ndarray | None): @property def model(self) -> np.ndarray: - """Physical property model""" + """The model associated with regularization. + + Returns + ------- + (n_param, ) numpy.ndarray + The model parameters. + """ return self._model @model.setter @@ -123,7 +142,13 @@ def model(self, values: np.ndarray | float): @property def mapping(self) -> maps.IdentityMap: - """Mapping applied to the model values""" + """Mapping from the model to the regularization mesh. + + Returns + ------- + SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. + """ return self._mapping @mapping.setter @@ -139,7 +164,15 @@ def mapping(self, mapping: maps.IdentityMap): @property def units(self) -> str | None: - """Specify the model units. Special care given to 'radian' values""" + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ return self._units @units.setter @@ -173,7 +206,13 @@ def _weights_shapes(self) -> tuple[int] | str: @property def reference_model(self) -> np.ndarray: - """Reference physical property model""" + """Reference model. + + Returns + ------- + (n_param, ) numpy.ndarray + Reference model. + """ return self._reference_model @reference_model.setter @@ -198,7 +237,15 @@ def reference_model(self, values: np.ndarray | float): @property def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" + """Regularization mesh. + + Mesh on which the regularization is defined. This is not the same as the mesh on which the simulation is defined. + + Returns + ------- + discretize.base.RegularizationMesh + Mesh on which the regularization is defined. + """ return self._regularization_mesh regmesh = deprecate_property( @@ -269,8 +316,21 @@ def remove_weights(self, key): @property def W(self) -> np.ndarray: - """ - Weighting matrix + r"""Weighting matrix. + + For the set of (n_cells, ) numpy arrays :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` + defining cell weights applied in the regularization, this method returns the cell weighting + matrix :math:`\mathbf{W}`, where: + + .. math:: + \mathbf{W} = diag \bigg ( \prod_i \, \mathbf{w_i} \bigg ) + + In this case, the product represents elementwise multiplications; i.e. the Hadamard product. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the regularization. """ if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) @@ -323,19 +383,25 @@ def f_m_deriv(self, m) -> csr_matrix: @utils.timeIt def deriv(self, m) -> np.ndarray: - r""" - The regularization is: + r"""Returns the gradient of the regularization function for the model provided. + + Where :math:`\phi_m` represents the regularization function, + this method returns the gradient: .. math:: + \nabla_m \phi_m = \frac{\partial \phi_m}{\partial \mathbf{m}} \bigg |_\mathbf{m} - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top - W(m-m_\text{ref})} + evaluated at the model :math:`(\mathbf{m})` provided. - So the derivative is straight forward: - - .. math:: + Parameters + ---------- + (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. - R(m) = \mathbf{W^\top W (m-m_\text{ref})} + Returns + ------- + (n_param, ) numpy.ndarray + The gradient of the regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) @@ -343,26 +409,32 @@ def deriv(self, m) -> np.ndarray: @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: - r""" - Second derivative + r"""Returns the second derivative of the regularization function. + + Where :math:`\phi_m` represents the regularization function, + this method returns either the second derivative evaluated at the model :math:`(\mathbf{m})` provided - :param numpy.ndarray m: geophysical model - :param numpy.ndarray v: vector to multiply - :rtype: scipy.sparse.csr_matrix - :return: WtW, or if v is supplied WtW*v (numpy.ndarray) + .. math:: + \nabla_m^2 \phi_m = \frac{\partial^2 \phi_m}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} - The regularization is: + or the second-derivative multiplied by a given vector :math:`(\mathbf{v})` .. math:: + \big [ \nabla_m^2 \phi_m \big ] \mathbf{v} = \bigg [ \frac{\partial^2 \phi_m}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} \bigg ] \mathbf{v} - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top - W(m-m_\text{ref})} - - So the second derivative is straight forward: - .. math:: + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector - R(m) = \mathbf{W^\top W} + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the second-derivative of the regularization function for the model + provided is returned. If *v* is not ``None``, the second-derivative multiplied by the vector provided is returned. """ f_m_deriv = self.f_m_deriv(m) @@ -373,34 +445,45 @@ def deriv2(self, m, v=None) -> csr_matrix: class Smallness(BaseRegularization): - r""" - Small regularization - L2 regularization on the difference between a - model and a reference model. + r"""Smallness least-squares regularization. + + Smallness least-squares regularization is used to ensure recovered model + values are not overly large in amplitude. In continuous form, the ``Smallness`` + regularization function is given by: .. math:: + r(m) = \frac{1}{2} \int_\Omega \, w(r) m(r)^2 \, dv - r(m) = \frac{1}{2}(\mathbf{m} - \mathbf{m_ref})^\top \mathbf{V}^T - \mathbf{W}^T - \mathbf{W} \mathbf{V} (\mathbf{m} - \mathbf{m_{ref}}) + where :math:`w(r)` is a user-defined weighting function. + In discrete form, the ``Smallness`` regularization function is given by: + + .. math:: + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{V_{\frac{1}{2}}^T} + \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}}} (\mathbf{m} - \mathbf{m_{ref}}) where - :math:`\mathbf{m}` is the model, - :math:`\mathbf{m_{ref}}` is a reference model, - :math:`\mathbf{V}` are square root of cell volumes and - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or - free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}` is a reference model, + - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). - **Optional Inputs** - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param int shape: number of parameters - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the size - of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights + Parameters + ---------- + mesh : discretize.base.BaseMesh mesh + The mesh on which the regularization is defined + shape : int + The number of model parameters + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the regularization mesh. """ @@ -424,23 +507,54 @@ def f_m_deriv(self, m) -> csr_matrix: class SmoothnessFirstOrder(BaseRegularization): - """ - Smooth Regularization. This base class regularizes on the first - spatial derivative, optionally normalized by the base cell size. - - **Optional Inputs** - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the - size of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights - :param bool reference_model_in_smooth: include the reference model in the - smoothness computation? (eg. look at Deriv of m (False) or Deriv of - (m-reference_model) (True)) - :param numpy.ndarray weights: vector of cell weights (applied in all terms) + r"""First-order smoothness least-squares regularization. + + First-order smoothness least-squares regularization is used to enforce spatial smoothness + in the recovered model along a specified direction. The regularization accomplishes this by + penalizing models with large first-order spatial derivatives along a specified direction. + + For a ``SmoothnessFirstOrder`` regularization that enforces smoothness along the x-direction, + the continuous regularization function is given by: + + .. math:: + r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \, dv + + where :math:`w(r)` is a user-defined weighting function. + In discrete form, this regularization function is given by: + + .. math:: + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T V_{\frac{1}{2}}^T} + \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}} G_x } (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}` is a reference model (optional), + - :math:`\mathbf{G_x}` is the partial gradient operator along the x-direction (i.e. x-derivative), + - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + + + Parameters + ---------- + mesh : discretize.base.BaseMesh mesh + The mesh on which the regularization is defined. + orientation : str {'x', 'y', 'z'} + The direction along which smoothness is enforced. Default = 'x'. + reference_model_in_smooth : bool + Whether the reference model is included in the smoothness regularization. Default = ``False``. + shape : int + The number of model parameters + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the regularization mesh. + """ def __init__( @@ -564,24 +678,54 @@ def orientation(self): class SmoothnessSecondOrder(SmoothnessFirstOrder): - """ - This base class regularizes on the second - spatial derivative, optionally normalized by the base cell size. - - **Optional Inputs** - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param int nP: number of parameters - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the - size of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights - :param bool reference_model_in_smooth: include the reference model in the - smoothness computation? (eg. look at Deriv of m (False) or Deriv of - (m-reference_model) (True)) - :param numpy.ndarray weights: vector of cell weights (applied in all terms) + r"""Second-order smoothness (flatness) least-squares regularization. + + Second-order smoothness least-squares regularization is used to enforce flatness + in the recovered model along a specified direction. The regularization accomplishes this by + penalizing models with large second-order spatial derivatives along a specified direction. + + For a ``SmoothnessSecondOrder`` regularization that enforces flatness along the x-direction, + the continuous regularization function is given by: + + .. math:: + r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial^2 m}{\partial x^2} \bigg )^2 \, dv + + where :math:`w(r)` is a user-defined weighting function. + In discrete form, this regularization function is given by: + + .. math:: + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x^T V_{\frac{1}{2}}^T} + \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}} L_x } (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}` is a reference model (optional), + - :math:`\mathbf{L_x}` is the second-order scalar derivative with respect to x, + - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + + + Parameters + ---------- + mesh : discretize.base.BaseMesh mesh + The mesh on which the regularization is defined. + orientation : str {'x', 'y', 'z'} + The direction along which smoothness is enforced. Default = 'x'. + reference_model_in_smooth : bool + Whether the reference model is included in the smoothness regularization. Default = ``False``. + shape : int + The number of model parameters + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the regularization mesh. + """ def f_m(self, m): @@ -660,6 +804,7 @@ class WeightedLeastSquares(ComboObjectiveFunction): mapping : SimPEG.maps.IdentityMap, optional A mapping to apply to the model before regularization. reference_model : array_like, optional + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. weights : None, array_like, or dict or array_like, optional From 70a71d45315fa4f52838768cbf7ec32043d654c4 Mon Sep 17 00:00:00 2001 From: dccowan Date: Fri, 5 May 2023 23:26:37 -0700 Subject: [PATCH 073/455] first pass of base regularization docstrings --- SimPEG/regularization/__init__.py | 33 +- SimPEG/regularization/base.py | 699 +++++++++++++++++++++++++----- SimPEG/regularization/sparse.py | 31 +- 3 files changed, 637 insertions(+), 126 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 99308fa187..ce4b3cd9a9 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -21,7 +21,7 @@ the subsurface structures responsible for the observed geophysical response. To accomplish this, regularization functions are used to ensure the solution to the inverse problem is unique and is geologically plausible. The choice in regularization function(s) depends on user assumptions and -a-prior information. +a priori information. SimPEG uses a deterministic inversion approach to recover an appropriate model. The algorithm does this by finding the model (:math:`m`) which minimizes a global objective function (or penalty function) of the form: @@ -47,24 +47,27 @@ model are not too large and are spatially smooth in the x and y-directions can be expressed as: .. math:: - \phi_m (m) = \int_\Omega \, \bigg [ \, - \frac{\alpha_s}{2} \, m^2 + - \frac{\alpha_x}{2} \bigg ( \frac{\partial m}{\partial x} \bigg )^2 + - \frac{\alpha_y}{2} \bigg ( \frac{\partial m}{\partial y} \bigg )^2 - \bigg ] \, d v + \phi_m (m) = + \alpha_s \! \int_\Omega \Bigg [ \frac{1}{2} w_s(r) \, m(r)^2 \Bigg ] \, dv + + \alpha_x \! \int_\Omega \Bigg [ \frac{1}{2} w_x(r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + + \alpha_y \! \int_\Omega \Bigg [ \frac{1}{2} w_y(r) \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv +where :math:`w_s(r), w_x(r), w_y(r)` are user-defined weighting functions. Discretized to a numerical grid (or mesh), the model becomes a discrete vector :math:`\mathbf{m}`. -And the aforementioned expression is approximately by: +And the aforementioned expression is approximated by: .. math:: - \phi_m (\mathbf{m}) & \approx \frac{\alpha_s}{2} \mathbf{m^T V^T V m} + - \frac{\alpha_x}{2} \mathbf{m^T V^T G_x^T G_x V m} + - \frac{\alpha_y}{2} \mathbf{m^T V^T G_y^T G_y V m} - = \frac{1}{2} \mathbf{m^T R^T R m} - -where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradients along the x and y-directions -respectively, and :math:`\mathbf{V}` is a diagonal matrix containing the square-roots of the cell -volumes. As is the case with multiple least-squares regularization functions, the terms can be amalgamated + \begin{align} + \phi_m (\mathbf{m}) &\approx \frac{\alpha_s}{2} \mathbf{m^T W_s^T W_s m} + + \frac{\alpha_x}{2} \mathbf{m^T G_x^T W_x^T W_x G_x m} + + \frac{\alpha_y}{2} \mathbf{m^T G_y^T W_y^T W_y G_y m} \\ + &\approx \frac{1}{2} \mathbf{m^T R^T R m} + \end{align} + +where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradients along the x and y-directions, respectively. +:math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` are weighting matrices that apply +user-defined weights and account for cell dimensions in the integration. +As is the case with multiple least-squares regularization functions, the terms can be amalgamated and used to define the model objective function using a single regularization operator :math:`\mathbf{R}`. diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 64b3d2c5a1..1d22c1c824 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -23,10 +23,11 @@ class BaseRegularization(BaseObjectiveFunction): Parameters ---------- - mesh : discretize.base.BaseMesh + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, numpy.ndarray of bool - Array of bool defining the set of regularization mesh cells that are active in the inversion. If ``None``, all cells are active. + Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. + If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -35,9 +36,8 @@ class BaseRegularization(BaseObjectiveFunction): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the regularization mesh. + the :py:class:`~SimPEG.regularization.RegularizationMesh`. - """ _model = None @@ -81,10 +81,11 @@ def __init__( @property def active_cells(self) -> np.ndarray: - """A boolean array of active cells, defined on the regularization mesh. + """Active cells defined on the regularization mesh. - Defines the cells in the regularization mesh that are active throughout the inversion. - Inactive cells remain fixed and are defined according to the starting model. + A boolean array defining the cells in the :py:class:`~SimPEG.regularization.RegularizationMesh` + that are active throughout the inversion. Inactive cells remain fixed and are defined according + to the starting model. Returns ------- @@ -92,8 +93,8 @@ def active_cells(self) -> np.ndarray: Notes ----- - If this is set with an array of integers, it interprets it as an array - listing the active cell indices. When called, the quantity will have + If the property is set using an array of integers, the setter interprets the array as + representing the indices of the active cells. When called however, the quantity will have been converted to a boolean array. """ return self.regularization_mesh.active_cells @@ -147,7 +148,7 @@ def mapping(self) -> maps.IdentityMap: Returns ------- SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. + The mapping from the model parameters to the quantity defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. """ return self._mapping @@ -277,14 +278,31 @@ def cell_weights(self, value): self.set_weights(cell_weights=value) def get_weights(self, key) -> np.ndarray: - """Weights for a given key.""" + """Cell weights for a given key. + + Returns + ------- + (n_cells, ) numpy.ndarray + Cell weights for a given key. + + Examples + -------- + >>> import discretize + >>> from SimPEG.regularization import Smallness + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Smallness(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + + """ return self._weights[key] def set_weights(self, **weights): """Adds (or updates) the specified weights to the regularization - Parameters: - ----------- + Parameters + ---------- **kwargs : key, numpy.ndarray Each keyword argument is added to the weights used by the regularization. They can be accessed with their keyword argument. @@ -298,6 +316,7 @@ def set_weights(self, **weights): >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) >>> reg.get_weights('my_weight') array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + """ for key, values in weights.items(): values = validate_ndarray_with_shape( @@ -307,7 +326,24 @@ def set_weights(self, **weights): self._W = None def remove_weights(self, key): - """Removes the weights with a given key""" + """Removes the weights for the key provided + + Parameters + ---------- + key : str + The key for the weights being removed from the cell weights dictionary. + + Examples + -------- + >>> import discretize + >>> from SimPEG.regularization import Smallness + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Smallness(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + >>> reg.remove_weights('my_weight') + """ try: self._weights.pop(key) except KeyError as error: @@ -383,8 +419,8 @@ def f_m_deriv(self, m) -> csr_matrix: @utils.timeIt def deriv(self, m) -> np.ndarray: - r"""Returns the gradient of the regularization function for the model provided. - + r"""Gradient of the regularization function for the model provided. + Where :math:`\phi_m` represents the regularization function, this method returns the gradient: @@ -409,8 +445,8 @@ def deriv(self, m) -> np.ndarray: @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: - r"""Returns the second derivative of the regularization function. - + r"""Second derivative of the regularization function. + Where :math:`\phi_m` represents the regularization function, this method returns either the second derivative evaluated at the model :math:`(\mathbf{m})` provided @@ -447,26 +483,26 @@ def deriv2(self, m, v=None) -> csr_matrix: class Smallness(BaseRegularization): r"""Smallness least-squares regularization. - Smallness least-squares regularization is used to ensure recovered model + Smallness least-squares regularization is used to ensure recovered model parameter values are not overly large in amplitude. In continuous form, the ``Smallness`` regularization function is given by: .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w(r) m(r)^2 \, dv + r(m) = \frac{1}{2} \int_\Omega \, w(r) \, m(r)^2 \, dv where :math:`w(r)` is a user-defined weighting function. In discrete form, the ``Smallness`` regularization function is given by: .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{V_{\frac{1}{2}}^T} - \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}}} (\mathbf{m} - \mathbf{m_{ref}}) + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{V^T} + \mathbf{W}^T \mathbf{W} \mathbf{V} (\mathbf{m} - \mathbf{m_{ref}}) where - :math:`\mathbf{m}` is the model, - :math:`\mathbf{m_{ref}}` is a reference model, - - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. Parameters @@ -483,7 +519,7 @@ class Smallness(BaseRegularization): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the regularization mesh. + the :py:class:`~SimPEG.regularization.RegularizationMesh`. """ @@ -494,14 +530,87 @@ def __init__(self, mesh, **kwargs): self.set_weights(volume=self.regularization_mesh.vol) def f_m(self, m) -> np.ndarray: - """ - Model residual + r"""Evaluate least squares regularization kernel. + + For ``Smallness`` regularization, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel. + + Notes + ----- + The discretized form of the smallness regularization function is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. The regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + + where :math:`\mathbf{m_{ref}}` is a reference model. """ return self.mapping * self._delta_m(m) def f_m_deriv(self, m) -> csr_matrix: - """ - Derivative of the model residual + r"""Derivative of the least-squares regularization kernel. + + For ``Smallness`` regularization, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + + And thus, the derivative with respect to the model is the identity matrix :math:`\mathbf{I}`. + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the least-squares regularization kernel. + + Notes + ----- + The discretized form of the smallness regularization function function is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. The regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + + where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the + model is: + + .. math:: + \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{I} """ return self.mapping.deriv(self._delta_m(m)) @@ -517,22 +626,22 @@ class SmoothnessFirstOrder(BaseRegularization): the continuous regularization function is given by: .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \, dv + r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \, dv where :math:`w(r)` is a user-defined weighting function. In discrete form, this regularization function is given by: .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T V_{\frac{1}{2}}^T} - \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}} G_x } (\mathbf{m} - \mathbf{m_{ref}}) + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T V}^T} + \mathbf{W}^T \mathbf{W} \mathbf{V} G_x } (\mathbf{m} - \mathbf{m_{ref}}) where - :math:`\mathbf{m}` is the model, - :math:`\mathbf{m_{ref}}` is a reference model (optional), - :math:`\mathbf{G_x}` is the partial gradient operator along the x-direction (i.e. x-derivative), - - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. Parameters @@ -553,7 +662,7 @@ class SmoothnessFirstOrder(BaseRegularization): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the regularization mesh. + the :py:class:`~SimPEG.regularization.RegularizationMesh`. """ @@ -582,7 +691,7 @@ def __init__( @property def _weights_shapes(self): - """Acceptable lengths for the weights + """Acceptable lengths for the weights. Returns ------- @@ -596,7 +705,17 @@ def _weights_shapes(self): @property def cell_gradient(self): - """Cell gradient operator""" + """Partial cell gradient operator. + + Returns the partial gradient operator which takes the derivative along the orientation + where smoothness is being enforced. For smoothness along the x-direction, the resulting operator would + map from cell centers to x-faces. + + Returns + ------- + scipy.sparse.csr_matrix + Partial cell gradient operator defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + """ if getattr(self, "_cell_gradient", None) is None: self._cell_gradient = getattr( self.regularization_mesh, "cell_gradient_{}".format(self.orientation) @@ -629,8 +748,44 @@ def _multiplier_pair(self): return f"alpha_{self.orientation}" def f_m(self, m): - """ - Model gradient + r"""Evaluate least squares regularization kernel. + + For ``SmoothnessFirstOrder`` regularization in the x-direction, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel. + + Notes + ----- + The discretized form of the smoothness regularization function in along the x-direction is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x^T W^T W G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{m_{ref}}` is a reference model. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -642,16 +797,64 @@ def f_m(self, m): return dfm_dl def f_m_deriv(self, m) -> csr_matrix: - """ - Derivative of the model gradient + r"""Derivative of the least-squares regularization kernel. + + For ``SmoothnessFirstOrder`` regularization, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + And thus, the derivative with respect to the model is the x-derivative operator :math:`\mathbf{G_x}`. + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the least-squares regularization kernel. + + Notes + ----- + The discretized form of the smoothness regularization function in along the x-direction is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x^T W^T W G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the + model is: + + .. math:: + \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} """ return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) @property def W(self): - """ - Weighting matrix that takes the volumes, free weights, fixed weights and - length scales of the difference operator (normalized optional). + """Weighting matrix. + + A sparse, diagonal weighting matrix for all weights associated with the + regularization object. This includes default weights that are set when the regularization object + is instantiated (e.g. cell volumes and length scales corresponding to the + difference operator), as well as any user-defined weights. + + Returns + ------- + scipy.sparse.csr_matrix + Weighting matrix. """ if getattr(self, "_W", None) is None: average_cell_2_face = getattr( @@ -674,6 +877,14 @@ def _cell_distances(self): @property def orientation(self): + """Direction along which smoothness is enforced. + + Returns + ------- + str {'x', 'y', 'z'} + The direction along which smoothness is enforced. Default = 'x'. + + """ return self._orientation @@ -688,22 +899,22 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): the continuous regularization function is given by: .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial^2 m}{\partial x^2} \bigg )^2 \, dv + r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial^2 m}{\partial x^2} \bigg )^2 \, dv where :math:`w(r)` is a user-defined weighting function. In discrete form, this regularization function is given by: .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x^T V_{\frac{1}{2}}^T} - \mathbf{W}^T \mathbf{W} \mathbf{V_{\frac{1}{2}} L_x } (\mathbf{m} - \mathbf{m_{ref}}) + r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x^T V}^T} + \mathbf{W}^T \mathbf{W} \mathbf{V} L_x } (\mathbf{m} - \mathbf{m_{ref}}) where - :math:`\mathbf{m}` is the model, - :math:`\mathbf{m_{ref}}` is a reference model (optional), - :math:`\mathbf{L_x}` is the second-order scalar derivative with respect to x, - - :math:`\mathbf{V_{\frac{1}{2}}}` are square root of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and + - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. Parameters @@ -724,13 +935,49 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the regularization mesh. + the :py:class:`~SimPEG.regularization.RegularizationMesh`. """ def f_m(self, m): - """ - Second model derivative + r"""Evaluate least squares regularization kernel. + + For ``SmoothnessSecondOrder`` regularization in the x-direction, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T} \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel. + + Notes + ----- + The discretized form of the smoothness regularization function in along the x-direction is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x G_x^T W^T W G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{m_{ref}}` is a reference model. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -745,8 +992,48 @@ def f_m(self, m): return dfm_dl2 def f_m_deriv(self, m) -> csr_matrix: - """ - Derivative of the second model residual + r"""Derivative of the least-squares regularization kernel. + + For ``SmoothnessSecondOrder`` regularization, the regularization kernel is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) + + And thus, the derivative with respect to the model is :math:`\mathbf{G_x^T G_x}`. + For a more detailed, description, see the notes. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the least-squares regularization kernel. + + Notes + ----- + The discretized form of the smoothness regularization function in along the x-direction is expressed as: + + .. math:: + \begin{align} + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x G_x^T W^T W G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \end{align} + + where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts + for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + + where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the + model is: + + .. math:: + \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{G_x^T G_x} """ return ( self.cell_gradient.T @@ -756,9 +1043,6 @@ def f_m_deriv(self, m) -> csr_matrix: @property def W(self): - """ - Weighting matrix - """ if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) self._W = utils.sdiag(weights**0.5) @@ -778,10 +1062,10 @@ def _multiplier_pair(self): class WeightedLeastSquares(ComboObjectiveFunction): - r"""Weighted least squares measure on model smallness and smoothness. + r"""Weighted least squares smallness and smoothness regularization. - L2 regularization with both smallness and smoothness (first order - derivative) contributions. + Construct a regularization using a weighted sum of smallness and smoothness + least-squares regularization functions. Parameters ---------- @@ -797,7 +1081,7 @@ class WeightedLeastSquares(ComboObjectiveFunction): First order smoothness weights for the respective dimensions. `None` implies setting these weights using the `length_scale` parameters. - alpha_xx, alpha_yy, alpha_zz : float, optional + alpha_xx, alpha_yy, alpha_zz : 0, float Second order smoothness weights for the respective dimensions. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. @@ -813,15 +1097,19 @@ class WeightedLeastSquares(ComboObjectiveFunction): Notes ----- - The function defined here approximates: + The regularization function approximates: .. math:: - \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + \phi_m(\mathbf{m}) &= \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ + & + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ + & + \alpha_{xx} \| W_{xx} \frac{\partial^2}{\partial x^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_{yy} \| W_{yy} \frac{\partial^2}{\partial y^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_{zz} \| W_{zz} \frac{\partial^2}{\partial z^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - Note if the key word argument `reference_model_in_smooth` is False, then mref is not + By default, :math:`\alpha_{xx}=\alpha_{yy}=\alpha_{zz}=0`. + Note if the key word argument `reference_model_in_smooth` is False, then :math:`m_{ref}` is not included in the smoothness contribution. If length scales are used to set the smoothness weights, alphas are respectively set internally using: @@ -939,12 +1227,47 @@ def __init__( self.set_weights(**weights) def set_weights(self, **weights): - """Update weights in children objective functions""" + """Adds (or updates) the specified weights for all child regularization objects. + + Parameters + ---------- + **kwargs : key, numpy.ndarray + Each keyword argument is added to the weights used by all child regularization objects. + They can be accessed with their keyword argument. + + Examples + -------- + >>> import discretize + >>> from SimPEG.regularization import WeightedLeastSquares + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = WeightedLeastSquares(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + + """ for fct in self.objfcts: fct.set_weights(**weights) def remove_weights(self, key): - """removes weights in children objective functions""" + """Removes specified weights from all child regularization objects. + + Parameters + ---------- + key : str + The key for the weights being removed from all child regularization objects. + + Examples + -------- + >>> import discretize + >>> from SimPEG.regularization import WeightedLeastSquares + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = WeightedLeastSquares(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + >>> reg.remove_weights('my_weight') + """ for fct in self.objfcts: fct.remove_weights(key) @@ -966,7 +1289,13 @@ def cell_weights(self, value): @property def alpha_s(self): - """smallness weight""" + """Weighting constant for smallness term. + + Returns + ------- + float + Weighting constant for smallness term. + """ return self._alpha_s @alpha_s.setter @@ -983,7 +1312,13 @@ def alpha_s(self, value): @property def alpha_x(self): - """weight for the first x-derivative""" + """Weighting constant for first order x-derivative term. + + Returns + ------- + float + Weighting constant for first order x-derivative term. + """ return self._alpha_x @alpha_x.setter @@ -998,7 +1333,13 @@ def alpha_x(self, value): @property def alpha_y(self): - """weight for the first y-derivative""" + """Weighting constant for first order y-derivative term. + + Returns + ------- + float + Weighting constant for first order y-derivative term. + """ return self._alpha_y @alpha_y.setter @@ -1013,7 +1354,13 @@ def alpha_y(self, value): @property def alpha_z(self): - """weight for the first z-derivative""" + """Weighting constant for first order z-derivative term. + + Returns + ------- + float + Weighting constant for first order z-derivative term. + """ return self._alpha_z @alpha_z.setter @@ -1028,7 +1375,13 @@ def alpha_z(self, value): @property def alpha_xx(self): - """weight for the second x-derivative""" + """Weighting constant for second order x-derivative term. + + Returns + ------- + float + Weighting constant for second order x-derivative term. + """ return self._alpha_xx @alpha_xx.setter @@ -1045,7 +1398,13 @@ def alpha_xx(self, value): @property def alpha_yy(self): - """weight for the second y-derivative""" + """Weighting constant for second order y-derivative term. + + Returns + ------- + float + Weighting constant for second order y-derivative term. + """ return self._alpha_yy @alpha_yy.setter @@ -1062,7 +1421,13 @@ def alpha_yy(self, value): @property def alpha_zz(self): - """weight for the second z-derivative""" + """Weighting constant for second order z-derivative term. + + Returns + ------- + float + Weighting constant for second order z-derivative term. + """ return self._alpha_zz @alpha_zz.setter @@ -1079,7 +1444,13 @@ def alpha_zz(self, value): @property def length_scale_x(self): - """Constant multiplier of the base length scale on model gradients along x.""" + """Constant multiplier of the base length scale on model gradients along x. + + Returns + ------- + float + Constant multiplier of the base length scale on model gradients along x. + """ return np.sqrt(self.alpha_x) / self.regularization_mesh.base_length @length_scale_x.setter @@ -1096,7 +1467,13 @@ def length_scale_x(self, value: float): @property def length_scale_y(self): - """Constant multiplier of the base length scale on model gradients along y.""" + """Constant multiplier of the base length scale on model gradients along y. + + Returns + ------- + float + Constant multiplier of the base length scale on model gradients along y. + """ return np.sqrt(self.alpha_y) / self.regularization_mesh.base_length @length_scale_y.setter @@ -1113,7 +1490,13 @@ def length_scale_y(self, value: float): @property def length_scale_z(self): - """Constant multiplier of the base length scale on model gradients along z.""" + """Constant multiplier of the base length scale on model gradients along z. + + Returns + ------- + float + Constant multiplier of the base length scale on model gradients along z. + """ return np.sqrt(self.alpha_z) / self.regularization_mesh.base_length @length_scale_z.setter @@ -1130,8 +1513,12 @@ def length_scale_z(self, value: float): @property def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. + """Whether to include the reference model in the smoothness terms. + + Returns + ------- + bool + Whether to include the reference model in the smoothness terms. """ return self._reference_model_in_smooth @@ -1150,8 +1537,12 @@ def reference_model_in_smooth(self, value: bool): # Other properties and methods @property def nP(self): - """ - number of model parameters + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. """ if getattr(self, "mapping", None) is not None and self.mapping.nP != "*": return self.mapping.nP @@ -1186,15 +1577,33 @@ def _delta_m(self, m): @property def multipliers(self): - """ - Factors that multiply the objective functions that are summed together - to build to composite regularization + """Multipliers for weighted sum of objective functions. + + Returns + ------- + list of float + Multipliers for weighted sum of objective functions. """ return [getattr(self, objfct._multiplier_pair) for objfct in self.objfcts] @property def active_cells(self) -> np.ndarray: - """Indices of active cells in the mesh""" + """Active cells defined on the regularization mesh. + + A boolean array defining the cells in the :py:class:`~SimPEG.regularization.RegularizationMesh` + that are active throughout the inversion. Inactive cells remain fixed and are defined according + to the starting model. + + Returns + ------- + (n_cells, ) Array of bool + + Notes + ----- + If the property is set using an array of integers, the setter interprets the array as + representing the indices of the active cells. When called however, the quantity will have + been converted to a boolean array. + """ return self.regularization_mesh.active_cells @active_cells.setter @@ -1216,7 +1625,13 @@ def active_cells(self, values: np.ndarray): @property def reference_model(self) -> np.ndarray: - """Reference physical property model""" + """Reference model values used to constrain the inversion. + + Returns + ------- + array_like + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + """ return self._reference_model @reference_model.setter @@ -1240,7 +1655,13 @@ def reference_model(self, values: np.ndarray | float): @property def model(self) -> np.ndarray: - """Physical property model""" + """The model associated with regularization. + + Returns + ------- + (n_param, ) numpy.ndarray + The model parameters. + """ return self._model @model.setter @@ -1255,7 +1676,15 @@ def model(self, values: np.ndarray | float): @property def units(self) -> str: - """Specify the model units. Special care given to 'radian' values""" + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ return self._units @units.setter @@ -1271,12 +1700,26 @@ def units(self, units: str | None): @property def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" + """Regularization mesh. + + Mesh on which the regularization is defined. This is not the same as the mesh on which the simulation is defined. + + Returns + ------- + discretize.base.RegularizationMesh + Mesh on which the regularization is defined. + """ return self._regularization_mesh @property def mapping(self) -> maps.IdentityMap: - """Mapping applied to the model values""" + """Mapping from the model to the regularization mesh. + + Returns + ------- + SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + """ return self._mapping @mapping.setter @@ -1301,18 +1744,24 @@ def mapping(self, mapping: maps.IdentityMap): # # ############################################################################### class BaseSimilarityMeasure(BaseRegularization): + """Base class for the similarity term in joint inversions. - """ - Base class for the similarity term in joint inversions. Inherit this for building - your own similarity term. The BaseSimilarityMeasure assumes two different - geophysical models through one similarity term. However, if you wish - to combine more than two models, e.g., 3 models, + The ``BaseSimilarityMeasure`` assumes two different geophysical models through one similarity term. + Inherit this for building your own similarity term. + However, if you wish to combine more than two models, e.g., 3 models, you may want to add a total of three coupling terms: e.g., lambda1*(m1, m2) + lambda2*(m1, m3) + lambda3*(m2, m3) where, lambdas are weights for coupling terms. m1, m2 and m3 indicate three different models. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh + Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + mapping : SimPEG.maps.WireMap + Wire map connecting physical properties defined on the regularization mesh to the entire model. """ def __init__(self, mesh, wire_map, **kwargs): @@ -1321,6 +1770,13 @@ def __init__(self, mesh, wire_map, **kwargs): @property def wire_map(self): + """Wire map connecting physical properties to the entire model. + + Returns + ------- + SimPEG.maps.WireMap + Wire map connecting physical properties defined on the regularization mesh to the entire model. + """ return self._wire_map @wire_map.setter @@ -1338,17 +1794,30 @@ def wire_map(self, wires): @property def nP(self): - """ - number of model parameters + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. """ return self.wire_map.nP def deriv(self, model): - """ - First derivative of the coupling term with respect to individual models. - Returns an array of dimensions [k*M,1], - k: number of models we are inverting for. - M: number of cells in each model. + """First derivative of the coupling term with respect to individual models. + + Where :math:`k` is the number of models we are inverting for and :math:`M` is the number of cells in each model, + this method returns a vector of length :math:`kM`. + + Parameters + ---------- + model : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + First derivative of the coupling term with respect to individual models. """ raise NotImplementedError( @@ -1358,12 +1827,24 @@ def deriv(self, model): ) def deriv2(self, model, v=None): - """ - Second derivative of the coupling term with respect to individual models. - Returns either an array of dimensions [k*M,1] (v is not None), or - sparse matrix of dimensions [k*M, k*M] (v is None). - k: number of models we are inverting for. - M: number of cells in each model. + """Second derivative of the coupling term with respect to individual models. + + Parameters + ---------- + model : numpy.ndarray + The model. + v : numpy.ndarray, optional + A vector. + + Returns + ------- + numpy.ndarray or scipy.sparse.csr_matrix + Where :math:`k` is the number of models we are inverting for and :math:`M` is the number of cells in each model, + this method returns: + + - an array of dimensions (k*M, ) when `v` is not ``None``. + - a sparse matrix of dimensions (k*M, k*M) when `v` is ``None``. + """ raise NotImplementedError( diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 071d0822d9..29937c8c40 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -21,8 +21,35 @@ class BaseSparse(BaseRegularization): - """ - Base class for building up the components of the Sparse Regularization + """Base class for sparse-norm regularization. + + The ``BaseSparse`` class defines properties and methods inherited by sparse-norm + regularization classes. Sparse-norm regularization in SimPEG is implemented using + an iteratively re-weighted least squares approach. It is not directly used to constrain the inversions. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + norm : float + The norm used in the regularization function. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. + If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the :py:class:`~SimPEG.regularization.RegularizationMesh`. + """ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): From 02f16fe2f35844febf60397c30db117b603abf44 Mon Sep 17 00:00:00 2001 From: dccowan Date: Tue, 9 May 2023 13:56:16 -0700 Subject: [PATCH 074/455] First pass, sparse regularization --- SimPEG/regularization/__init__.py | 6 +- SimPEG/regularization/base.py | 507 +++++++++++++++++++----------- SimPEG/regularization/sparse.py | 344 +++++++++++++++++--- 3 files changed, 626 insertions(+), 231 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index ce4b3cd9a9..a1178c2827 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -31,10 +31,10 @@ The global objective function contains two terms: a data misfit term :math:`\phi_d` which ensures data predicted by the recovered model adequately reproduces the observed data, and the model -objective function :math:`\phi_m` which is comprised of one or more regularization functions. I.e.: +objective function :math:`\phi_m` which is comprised of one or more regularization functions :math:`\gamma_i (m)`. I.e.: .. math:: - \phi_m (m) = \sum_i \alpha_i \, r_i (m) + \phi_m (m) = \sum_i \alpha_i \, \gamma_i (m) The model objective function imposes all of the desired constraints on the recovered model. Constants :math:`\alpha_i` weight the relative contributions of the regularization functions @@ -77,7 +77,7 @@ Weighted Least Squares Regularization ------------------------------------- Weighted least squares regularization functions are defined as weighted L2-norms on the model, its first-order -directional derivative(s), or its second-order direction derivative(s). +directional derivative(s), or its second-order directional derivative(s). .. autosummary:: :toctree: generated/ diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 1d22c1c824..e55e7c0b99 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -24,7 +24,7 @@ class BaseRegularization(BaseObjectiveFunction): Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, numpy.ndarray of bool Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. @@ -240,12 +240,12 @@ def reference_model(self, values: np.ndarray | float): def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. - Mesh on which the regularization is defined. This is not the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not the same as the mesh on which the simulation is defined. Returns ------- discretize.base.RegularizationMesh - Mesh on which the regularization is defined. + Mesh on which the regularization is discretized. """ return self._regularization_mesh @@ -354,14 +354,18 @@ def remove_weights(self, key): def W(self) -> np.ndarray: r"""Weighting matrix. - For the set of (n_cells, ) numpy arrays :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` - defining cell weights applied in the regularization, this method returns the cell weighting + For a set of (n_cells, ) numpy arrays :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` + representing custom cell weights applied in the regularization, this method returns the cell weighting matrix :math:`\mathbf{W}`, where: .. math:: - \mathbf{W} = diag \bigg ( \prod_i \, \mathbf{w_i} \bigg ) + \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \, \mathbf{w_i} \bigg )^{1/2} \bigg ] - In this case, the product represents elementwise multiplications; i.e. the Hadamard product. + The vector :math:`\mathbf{\tilde{v}}` accounts for cell volumes and dimensions + when the regularization function is discretized to the mesh. + + Weights are set using the `weights` property. For a comprehensive mathematical + description of the weighting matrix, see the *Notes* section for :class:`WeightedLeastSquares`. Returns ------- @@ -404,7 +408,7 @@ def __call__(self, m): .. math:: - r(m) = \frac{1}{2} \| \mathbf{W} \mathbf{f(m)} \|_2^2 + \gamma (m) = \frac{1}{2} \| \mathbf{W} \mathbf{f(m)} \|_2^2 """ r = self.W * self.f_m(m) return 0.5 * r.dot(r) @@ -419,15 +423,15 @@ def f_m_deriv(self, m) -> csr_matrix: @utils.timeIt def deriv(self, m) -> np.ndarray: - r"""Gradient of the regularization function for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. - Where :math:`\phi_m` represents the regularization function, - this method returns the gradient: + Where :math:`\gamma (\mathbf{m})` represents the discrete regularization function, + this method returns the derivative with respect to the model parameters: .. math:: - \nabla_m \phi_m = \frac{\partial \phi_m}{\partial \mathbf{m}} \bigg |_\mathbf{m} + \frac{\partial \gamma}{\partial \mathbf{m}} \bigg |_\mathbf{m} - evaluated at the model :math:`(\mathbf{m})` provided. + evaluated at the model :math:`\mathbf{m}` provided. Parameters ---------- @@ -445,18 +449,18 @@ def deriv(self, m) -> np.ndarray: @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: - r"""Second derivative of the regularization function. + r"""Second derivative of the regularization function evaluated for the model provided. - Where :math:`\phi_m` represents the regularization function, - this method returns either the second derivative evaluated at the model :math:`(\mathbf{m})` provided + Where :math:`\gamma (\mathbf{m})` represents the discrete regularization function, + this method returns the second derivative (Hessian) with respect to the model parameters: .. math:: - \nabla_m^2 \phi_m = \frac{\partial^2 \phi_m}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} + \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} or the second-derivative multiplied by a given vector :math:`(\mathbf{v})` .. math:: - \big [ \nabla_m^2 \phi_m \big ] \mathbf{v} = \bigg [ \frac{\partial^2 \phi_m}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} \bigg ] \mathbf{v} + \bigg [ \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} \bigg ] \mathbf{v} Parameters @@ -481,34 +485,22 @@ def deriv2(self, m, v=None) -> csr_matrix: class Smallness(BaseRegularization): - r"""Smallness least-squares regularization. - - Smallness least-squares regularization is used to ensure recovered model parameter - values are not overly large in amplitude. In continuous form, the ``Smallness`` - regularization function is given by: - - .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w(r) \, m(r)^2 \, dv - - where :math:`w(r)` is a user-defined weighting function. - In discrete form, the ``Smallness`` regularization function is given by: - - .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{V^T} - \mathbf{W}^T \mathbf{W} \mathbf{V} (\mathbf{m} - \mathbf{m_{ref}}) - - where - - - :math:`\mathbf{m}` is the model, - - :math:`\mathbf{m_{ref}}` is a reference model, - - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. - - + r"""Smallness regularization for least-squares inversion. + + ``Smallness`` regularization is used to ensure that differences between the + model values in the recovered model and the reference model are small; + i.e. it preserves structures in the reference model. If the `reference_model` argument is not + used to set a reference model, the starting model will be set as the + reference model in the regularization by default. Optionally, the `weights` argument can be used + to supply custom weights to control the degree of smallness being enforced + throughout different regions the model. + + See the *Notes* section below for a full mathematical description. + Parameters ---------- mesh : discretize.base.BaseMesh mesh - The mesh on which the regularization is defined + Mesh on which the regularization is discretized shape : int The number of model parameters mapping : None, SimPEG.maps.BaseMap @@ -521,6 +513,38 @@ class Smallness(BaseRegularization): Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Notes + ----- + The regularization function for smallness is defined as: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \big [ m(r) - m_{ref}(r) \big ]^2 \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` + and :math:`w(r)` are continuous variables as a function of location :math:`r`. + + For practical implementation within SimPEG, the regularization function and the aforementioned variables + are discretized onto a mesh (set upon instantiation). The discrete approximation to the regularization function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T + \mathbf{W}^T \mathbf{W} (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` are the discrete model parameters (model), + - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), and + - :math:`\mathbf{W}` is the weighting matrix. + + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; + optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions + when the regularization function is discretized to the mesh. """ _multiplier_pair = "alpha_s" @@ -530,14 +554,15 @@ def __init__(self, mesh, **kwargs): self.set_weights(volume=self.regularization_mesh.vol) def f_m(self, m) -> np.ndarray: - r"""Evaluate least squares regularization kernel. + r"""Evaluate least-squares regularization kernel. - For ``Smallness`` regularization, the regularization kernel is given by: + For ``Smallness`` regularization, the least-squares regularization kernel is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} - For a more detailed, description, see the notes. + where :math:`\mathbf{m}` are the descrete model parameters and :math:`\mathbf{m_{ref}}` + is a reference model. For a more detailed description, see the *Notes* section below. Parameters ---------- @@ -547,38 +572,43 @@ def f_m(self, m) -> np.ndarray: Returns ------- numpy.ndarray - The regularization kernel. + The least-squares regularization kernel. Notes ----- The discretized form of the smallness regularization function is expressed as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \phi_m (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. The regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` + is the reference model, and :math:`\mathbf{W}` is the weighting matrix. + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} - where :math:`\mathbf{m_{ref}}` is a reference model. + such that + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`Smallness` class. """ return self.mapping * self._delta_m(m) def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``Smallness`` regularization, the regularization kernel is given by: + For ``Smallness`` regularization, the derivative of the least-squares regularization kernel + with respect to the model is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{I} - And thus, the derivative with respect to the model is the identity matrix :math:`\mathbf{I}`. - For a more detailed, description, see the notes. + where :math:`\mathbf{I}` is the identity matrix: + For a more detailed description, see the *Notes* section below. Parameters ---------- @@ -592,25 +622,31 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the smallness regularization function function is expressed as: + The discretized form of the ``Smallness`` regularization function is expressed as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \phi_m (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. The regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` + is the reference model, and :math:`\mathbf{W}` is the weighting matrix. + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} - where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the - model is: + such that + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + + Thus, the derivate with respect to the model is: .. math:: - \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{I} + \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{I} + + where :math:`\mathbf{I}` is the identity matrix. + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`Smallness` class. """ return self.mapping.deriv(self._delta_m(m)) @@ -618,40 +654,23 @@ def f_m_deriv(self, m) -> csr_matrix: class SmoothnessFirstOrder(BaseRegularization): r"""First-order smoothness least-squares regularization. - First-order smoothness least-squares regularization is used to enforce spatial smoothness - in the recovered model along a specified direction. The regularization accomplishes this by - penalizing models with large first-order spatial derivatives along a specified direction. - - For a ``SmoothnessFirstOrder`` regularization that enforces smoothness along the x-direction, - the continuous regularization function is given by: - - .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \, dv - - where :math:`w(r)` is a user-defined weighting function. - In discrete form, this regularization function is given by: - - .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T V}^T} - \mathbf{W}^T \mathbf{W} \mathbf{V} G_x } (\mathbf{m} - \mathbf{m_{ref}}) - - where - - - :math:`\mathbf{m}` is the model, - - :math:`\mathbf{m_{ref}}` is a reference model (optional), - - :math:`\mathbf{G_x}` is the partial gradient operator along the x-direction (i.e. x-derivative), - - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. + ``SmoothnessFirstOrder`` regularization is used to ensure that values in the recovered model + are smooth along a specified direction. When the `reference_model` argument used to set a reference model, + the regularization preserves gradients/interfaces within the reference model along the direction + specified by the `orientation` argument. Optionally, the `weights` argument can be used + to supply custom weights to control the degree of smoothness being enforced + throughout different regions the model. + See the *Notes* section below for a full mathematical description. Parameters ---------- mesh : discretize.base.BaseMesh mesh - The mesh on which the regularization is defined. + The mesh on which the regularization is discretized. orientation : str {'x', 'y', 'z'} The direction along which smoothness is enforced. Default = 'x'. reference_model_in_smooth : bool - Whether the reference model is included in the smoothness regularization. Default = ``False``. + Whether the reference model is included in the smoothness regularization. If ``False``, it is equivalent to setting the reference model to 0. shape : int The number of model parameters mapping : None, SimPEG.maps.BaseMap @@ -664,6 +683,40 @@ class SmoothnessFirstOrder(BaseRegularization): Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Notes + ----- + The regularization function for first-order smoothness along the x-direction is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w (r) \Bigg ( \frac{\partial}{\partial x} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` + and :math:`w(r)` are continuous variables as a function of location :math:`r`. + + For practical implementation within SimPEG, the regularization function and the aforementioned variables + are discretized onto a mesh (set upon instantiation). The discrete approximation to the regularization function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x}^T + \mathbf{W}^T \mathbf{W} \mathbf{G_x} (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` are the discrete model parameters (model), + - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), + - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and + - :math:`\mathbf{W}` is the weighting matrix. + + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \mathbf{A_{fc}}^T \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; + optionally set using `weights`. :math:`\mathbf{A_{fc}}` averages from faces to cell centers + and :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions + when the regularization function is discretized to the mesh. """ def __init__( @@ -724,9 +777,7 @@ def cell_gradient(self): @property def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. - """ + # Inherited from BaseRegularization class return self._reference_model_in_smooth @reference_model_in_smooth.setter @@ -748,14 +799,15 @@ def _multiplier_pair(self): return f"alpha_{self.orientation}" def f_m(self, m): - r"""Evaluate least squares regularization kernel. + r"""Evaluate least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization in the x-direction, the regularization kernel is given by: + For ``SmoothnessFirstOrder`` regularization in the x-direction, the least-squares regularization kernel is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), + :math:`\mathbf{m}` are the descrite model parameters and :math:`\mathbf{m_{ref}}` is the reference model. For a more detailed, description, see the notes. Parameters @@ -766,11 +818,11 @@ def f_m(self, m): Returns ------- numpy.ndarray - The regularization kernel. + The least-squares regularization kernel. Notes ----- - The discretized form of the smoothness regularization function in along the x-direction is expressed as: + The discretized form of the first order smoothness regularization function along the x-direction is expressed as: .. math:: \begin{align} @@ -778,14 +830,16 @@ def f_m(self, m): &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} \end{align} - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. - :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` + is a reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along + the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. + Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{m_{ref}}` is a reference model. + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`SmoothnessFirstOrder` class. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -799,7 +853,7 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization, the regularization kernel is given by: + For ``SmoothnessFirstOrder`` regularization, the least-squares regularization kernel is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) @@ -819,7 +873,7 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the smoothness regularization function in along the x-direction is expressed as: + The discretized regularization function for smoothness along the x-direction is expressed as: .. math:: \begin{align} @@ -827,30 +881,42 @@ def f_m_deriv(self, m) -> csr_matrix: &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} \end{align} - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. - :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` + is a reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along + the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. + Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the - model is: + The derivate with respect to the model is therefore: .. math:: - \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{G_x} + + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`SmoothnessFirstOrder` class. """ return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) @property def W(self): - """Weighting matrix. + r"""Weighting matrix. A sparse, diagonal weighting matrix for all weights associated with the regularization object. This includes default weights that are set when the regularization object is instantiated (e.g. cell volumes and length scales corresponding to the difference operator), as well as any user-defined weights. + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \mathbf{A_{fc}}^T \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + The vector :math:`\mathbf{\tilde{v}}` accounts for cell volumes and dimensions + when the regularization function is discretized to the mesh. + And :math:`\mathbf{A_{fc}}` averages from faces to cell centers. + Returns ------- scipy.sparse.csr_matrix @@ -891,36 +957,19 @@ def orientation(self): class SmoothnessSecondOrder(SmoothnessFirstOrder): r"""Second-order smoothness (flatness) least-squares regularization. - Second-order smoothness least-squares regularization is used to enforce flatness - in the recovered model along a specified direction. The regularization accomplishes this by - penalizing models with large second-order spatial derivatives along a specified direction. - - For a ``SmoothnessSecondOrder`` regularization that enforces flatness along the x-direction, - the continuous regularization function is given by: - - .. math:: - r(m) = \frac{1}{2} \int_\Omega \, w (r) \bigg ( \frac{\partial^2 m}{\partial x^2} \bigg )^2 \, dv - - where :math:`w(r)` is a user-defined weighting function. - In discrete form, this regularization function is given by: - - .. math:: - r(\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x^T V}^T} - \mathbf{W}^T \mathbf{W} \mathbf{V} L_x } (\mathbf{m} - \mathbf{m_{ref}}) - - where - - - :math:`\mathbf{m}` is the model, - - :math:`\mathbf{m_{ref}}` is a reference model (optional), - - :math:`\mathbf{L_x}` is the second-order scalar derivative with respect to x, - - :math:`\mathbf{V}` is a diagonal matrix containing the **square roots** of cell volumes and - - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or free weights are provided, then :math:`\mathbf{W}` contains the **square roots** of the weights provided. + ``SmoothnessSecondOrder`` regularization is used to ensure that values in the recovered model + have small second-order spatial derivatives along a specified direction. When `reference_model` is used + to provide reference model, the regularization preserves second-order spatial derivatives within the + reference model along the direction defined by the `orientation` argument. + Optionally, the `weights` argument can be used to supply custom weights to control the degree of + smoothness being enforced throughout different regions the model. + See the *Notes* section below for a full mathematical description. Parameters ---------- mesh : discretize.base.BaseMesh mesh - The mesh on which the regularization is defined. + The mesh on which the regularization is discretized. orientation : str {'x', 'y', 'z'} The direction along which smoothness is enforced. Default = 'x'. reference_model_in_smooth : bool @@ -937,17 +986,50 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Notes + ----- + The regularization function for second-order smoothness along the x-direction is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w (r) \Bigg ( \frac{\partial^2}{\partial x^2} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` + and :math:`w(r)` are continuous variables as a function of location :math:`r`. + + For practical implementation, the regularization function and the aforementioned variables + are discretized onto a mesh; which is set upon instantiation. The discrete approximation to the regularization function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x}^T + \mathbf{W}^T \mathbf{W} \mathbf{L_x} (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` are the discrete model parameters (model), + - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), + - :math:`\mathbf{L_x}` is a second-order derivative operator with respect to :math:`x`, and + - :math:`\mathbf{W}` is the weighting matrix. + + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; + optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions + when the regularization function is discretized to the mesh. """ def f_m(self, m): - r"""Evaluate least squares regularization kernel. + r"""Evaluate least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization in the x-direction, the regularization kernel is given by: + For ``SmoothnessSecondOrder`` regularization in the x-direction, the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T} \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator. For a more detailed, description, see the notes. Parameters @@ -958,26 +1040,29 @@ def f_m(self, m): Returns ------- numpy.ndarray - The regularization kernel. + The least-squares regularization kernel. Notes ----- - The discretized form of the smoothness regularization function in along the x-direction is expressed as: + The discretized form of the second-order smoothness regularization function along the x-direction is expressed as: .. math:: \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x G_x^T W^T W G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{L_x^T W^T W L_x} \big ( \mathbf{m - m_{ref}} \big ) \\ &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} \end{align} - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. - :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, + :math:`\mathbf{m}` are the dicrete model parameters, + :math:`\mathbf{m_{ref}}` is a reference model and :math:`\mathbf{W}` is a weighting + matrix that applies user-defined weights and accounts for cell dimensions in the integration. + Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{m_{ref}}` is a reference model. + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`SmoothnessSecondOrder` class. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -994,12 +1079,16 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization, the regularization kernel is given by: + For ``SmoothnessSecondOrder`` regularization, the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x}^T \big ( \mathbf{m - m_{ref}} \big ) - And thus, the derivative with respect to the model is :math:`\mathbf{G_x^T G_x}`. + And thus, the derivative with respect to the model is + + .. math:: + \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{L_x} + For a more detailed, description, see the notes. Parameters @@ -1018,22 +1107,26 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x G_x^T W^T W G_x^T G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ + \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{L_x^T W^T W L_x} \big ( \mathbf{m - m_{ref}} \big ) \\ &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} \end{align} - where :math:`\mathbf{G_x}` is the discrete x-derivative operator. - :math:`\mathbf{W}` is a weighting matrix that applies user-defined weights and accounts - for cell dimensions in the integration. Thus the regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, + :math:`\mathbf{m}` are the set of discrete model parameters, + :math:`\mathbf{m_{ref}}` is a reference model and :math:`\mathbf{W}` is a weighting + matrix that applies user-defined weights and accounts for cell dimensions in the integration. + Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) - where :math:`\mathbf{m_{ref}}` is a reference model. Thus, the derivate with respect to the - model is: + The derivate with respect to the model is: .. math:: - \frac{\partial \partial{f_m}}{\partial \mathbf{m}} = \mathbf{G_x^T G_x} + \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{L_x} + + For a more comprehensive description of the regularization function, see the *Notes* section + with documentation for the :class:`SmoothnessSecondOrder` class. """ return ( self.cell_gradient.T @@ -1043,6 +1136,7 @@ def f_m_deriv(self, m) -> csr_matrix: @property def W(self): + # Docstring inherited by BaseRegularization class. if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) self._W = utils.sdiag(weights**0.5) @@ -1062,15 +1156,35 @@ def _multiplier_pair(self): class WeightedLeastSquares(ComboObjectiveFunction): - r"""Weighted least squares smallness and smoothness regularization. + r"""Weighted least-squares regularization using smallness and smoothness. + + Apply regularization using a weighted sum of :class:`Smallness`, :class:`SmoothnessFirstOrder`, + and/or :class:`SmoothnessSecondOrder` (optional) least-squares regularization functions. + ``Smallness`` regularization is used to ensure that values in the recovered model, + or differences between the recovered model and a reference model, are not overly + large in magnitude. ``Smoothness`` regularizations are used to ensure that values in the recovered model + are smooth along specified directions. When `reference_in_smooth` is used to include the reference model + in the smoothness terms, the inversion preserves gradients/interfaces within the reference model. + the `weights` argument can be used to supply custom weights to control the degree of + smallness and smoothness being enforced throughout different regions the model. + + See the *Notes* section below for a full mathematical description of the regularization. + + By default, second-order smoothness is not included in the regularization; i.e. input parameters + `alpha_xx, alpha_yy, alpha_zz = 0`. And the reference model is not included in any smoothness terms; + i.e. `reference_model_in_smooth` is ``False``. The user may set the weighting constants + `alpha` directly, or indirectly using length scales such that: + + >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 + + and - Construct a regularization using a weighted sum of smallness and smoothness - least-squares regularization functions. + >>> alpha_xx = (length_scale_x * min(mesh.edge_lengths)) ** 4 Parameters ---------- mesh : discretize.base.BaseMesh - The mesh on which the model parameters are defined. This is used + The mesh on which the regularization is discretized. This is used for constructing difference operators for the smoothness terms. active_cells : array_like of bool or int, optional List of active cell indices, or a `mesh.n_cells` boolean array @@ -1082,7 +1196,8 @@ class WeightedLeastSquares(ComboObjectiveFunction): `None` implies setting these weights using the `length_scale` parameters. alpha_xx, alpha_yy, alpha_zz : 0, float - Second order smoothness weights for the respective dimensions. + Second order smoothness weights for the respective dimensions. By default, second order + smoothness is unused in the regularization. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. mapping : SimPEG.maps.IdentityMap, optional @@ -1091,29 +1206,45 @@ class WeightedLeastSquares(ComboObjectiveFunction): Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict or array_like, optional + weights : None, array_like, or dict of array_like, optional User defined weights. It is recommended to interact with weights using - the `get_weights`, `set_weights` functionality. + the :py:meth:`~get_weights`, :py:meth:`~set_weights` methods. Notes ----- - The regularization function approximates: + The model objective function :math:`\phi_m (m)` defined by the weighted sum of smallness and smoothness + regularization functions is given by: .. math:: - \phi_m(\mathbf{m}) &= \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ - & + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ - & + \alpha_{xx} \| W_{xx} \frac{\partial^2}{\partial x^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_{yy} \| W_{yy} \frac{\partial^2}{\partial y^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_{zz} \| W_{zz} \frac{\partial^2}{\partial z^2} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - - By default, :math:`\alpha_{xx}=\alpha_{yy}=\alpha_{zz}=0`. - Note if the key word argument `reference_model_in_smooth` is False, then :math:`m_{ref}` is not - included in the smoothness contribution. - - If length scales are used to set the smoothness weights, alphas are respectively set internally using: - >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 + \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) \Big [ m(r) - m_{ref}(r) \Big ]^2 \, dv \\ + &+ \sum_{i=x,y,z} \frac{\alpha_i}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial}{\partial \xi_i} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv \\ + &+ \sum_{i=x,y,z} \frac{\alpha_{ii}}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial^2}{\partial \xi_i^2} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model that may or may not be + included in each term, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_i` is + the unit direction along :math:`i`. Constants :math:`\alpha_s`, :math:`\alpha_i` and + :math:`\alpha_{ii}` (optional) weight the respective contributions of the smallness, first-order smoothness, + and second-order smoothness regularization functions. By our definition, + :math:`m(r)`, :math:`m_{ref}(r)` and :math:`w(r)` are continuous variables as a function of location :math:`r`. + + For practical implementation, the model objective function and the aforementioned variables + are discretized onto a mesh; set upon instantiation. The discrete approximation to the model objective function is given by: + + .. math:: + \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} \| \mathbf{W_s} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ + &+ \sum_{i=x,y,z} \frac{\alpha_i}{2} \| \mathbf{W_i G_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ + &+ \sum_{ii=x,y,z} \frac{\alpha_{ii}}{2} \| \mathbf{W_i L_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + where + + - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), + - :math:`\mathbf{m_{ref}}` is a reference model which may or may not be inclulded in the smoothess terms, + - :math:`\mathbf{G_i}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_i}` are second-order derivative operators for x, y and z, and + - :math:`\mathbf{W}` are weighting matrices. + + See the documentation for :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` + for more details on how the weighting matrices are constructed. """ _model = None @@ -1702,12 +1833,12 @@ def units(self, units: str | None): def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. - Mesh on which the regularization is defined. This is not the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not the same as the mesh on which the simulation is defined. Returns ------- discretize.base.RegularizationMesh - Mesh on which the regularization is defined. + Mesh on which the regularization is discretized. """ return self._regularization_mesh @@ -1759,7 +1890,7 @@ class BaseSimilarityMeasure(BaseRegularization): Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh - Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. mapping : SimPEG.maps.WireMap Wire map connecting physical properties defined on the regularization mesh to the entire model. """ diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 29937c8c40..91f1c4a523 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -25,14 +25,15 @@ class BaseSparse(BaseRegularization): The ``BaseSparse`` class defines properties and methods inherited by sparse-norm regularization classes. Sparse-norm regularization in SimPEG is implemented using - an iteratively re-weighted least squares approach. It is not directly used to constrain the inversions. + an iteratively re-weighted least squares (IRLS) approach. The ``BaseSparse`` class + however, is not directly used to define the regularization for the inverse problem. Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is defined. This is not necessarily the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. norm : float - The norm used in the regularization function. + The norm used in the regularization function. Must be between within the interval [0, 2]. irls_scaled : bool If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. irls_threshold : float @@ -49,7 +50,6 @@ class BaseSparse(BaseRegularization): weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. - """ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): @@ -60,8 +60,15 @@ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwar @property def irls_scaled(self) -> bool: - """ - Scale irls weights. + """Scale IRLS weights. + + When ``True``, the iteratively re-weighted least-squares (IRLS) weights are scaled at + each update to generally preserve the magnitude of the regularization term. + + Returns + ------- + bool + Whether to scale IRLS weights. """ return self._irls_scaled @@ -71,8 +78,20 @@ def irls_scaled(self, value: bool): @property def irls_threshold(self): - """ - Constant added to the denominator of the IRLS weights for stability. + r"""Constant added to the denominator of the IRLS weights for stability. + + At each IRLS iteration, the IRLS weights are updated. + The weight at iteration :math:`k` corresponding the cell :math:`i` is given by: + + .. math:: + r_i (\mathbf{m}^{(k)}) = \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} + + where the `irls_threshold` :math:`\epsilon` is a constant that stabilizes the expression. + + Returns + ------- + float + Constant added to the denominator of the IRLS weights for stability. """ return self._irls_threshold @@ -84,8 +103,26 @@ def irls_threshold(self, value): @property def norm(self): - """ - Value of the norm + r"""Norm for the sparse regularization. + + Sparse-norm regularization functions within SimPEG take the form: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big ( \mathbb{F} \big [ m(r) - m_{ref}(r) \big ] \Bigg )^p \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. :math:`\mathbb{F}` is a placeholder for a function that + acts on the difference between :math:`m` and :math:`m_{ref}`; e.g. a differential operator. + + The parameter :math:`p \in [0,2]` defines the norm, where a smaller norm is used to a model + with more sparse structures. + + Returns + ------- + None, float, (n_cells, ) numpy.ndarray + Norm for the sparse regularization. If ``None``, a 2-norm is used. + A float within the interval [0,2] represents a constant norm applied for all cells. + A ``numpy.ndarray`` object, where each entry is used to apply a different norm to each cell in the mesh. """ return self._norm @@ -109,10 +146,16 @@ def norm(self, value: float | np.ndarray | None): self._norm = value def get_lp_weights(self, f_m): - """ - Utility function to get the IRLS weights. - By default, the weights are scaled by the gradient of the IRLS on - the max of the l2-norm. + """Compute and return standard IRLS weights. + + The IRLS weights are applied to ensure the discrete approximation of the sparse + regularization function is evaluated using the specified norm. Since the weighting + is model dependent, it must be recomputed at every IRLS iteration. + + Parameters + ---------- + f_m : fcn + The least-squares regularization kernel. """ lp_scale = np.ones_like(f_m) if self.irls_scaled: @@ -133,27 +176,186 @@ def get_lp_weights(self, f_m): class SparseSmallness(BaseSparse, Smallness): - """ - Sparse smallness regularization + r"""Sparse smallness (compactness) regularization. - **Inputs** + ``SparseSmallness`` is used to recover models comprised of compact structures. + The level of compactness is controlled by the `norm` within the regularization function; + with more compact structures being recovered when a smaller norm is used. + Optionally, the `weights` argument can be used to supply custom weights to control the degree of compactness being enforced + throughout different regions the model. - :param int norm: norm on the smallness + See the *Notes* section below for a full mathematical description. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. + norm : float + The norm used in the regularization function. Must be within the interval [0, 2]. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. + If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the :py:class:`~SimPEG.regularization.RegularizationMesh`. + + Notes + ----- + The regularization function for sparse smallness (compactness) is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \big [ m(r) - m_{ref}(r) \big ]^p \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` + and :math:`w(r)` are continuous variables as a function of location :math:`r`. + The parameter :math:`p \in [0,2]` is defined using the `norm` input argument, + where a smaller norm is used to recovery more compact structures. + + For practical implementation, the regularization function and the aforementioned variables + are discretized onto a mesh. The discrete approximation to the regularization function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{W^T R^T R W} (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` are the discrete model parameters, + - :math:`\mathbf{m_{ref}}` is a reference model (optional, set using `reference_model`), + - :math:`\mathbf{W}` is the weighting matrix, and + - :math:`\mathbf{R}` is a model-dependent re-weighting matrix that is updated at every IRLS iteration. + + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; + optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions + when the regularization function is discretized to the mesh. + + The re-weighting matrix :math:`\mathbf{R}` is responsible for evaluating the proper norm in the regularization function. + :math:`\mathbf{R}` is a sparse diagonal matrix whose elements are recomputed at every IRLS iteration. For IRLS iteration :math:`k`, the + :math:`i^{th}` diagonal element is computed as follows: + + .. math:: + R_{ii}^{(k)} = \bigg [ \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} \bigg ]^{1/2} + + where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). """ _multiplier_pair = "alpha_s" def update_weights(self, m): - """ - Compute and store the irls weights. + r"""Compute and update the IRLS weights. + + The re-weighting matrix :math:`\mathbf{R}` ensure the specified norm is evaluated in the regularization function. + For a mathematically description of IRLS weights, see the *Notes* section in the :class:`SparseSmallness` class documentation. + + Parameters + ---------- + m : numpy.ndarray + The model. """ f_m = self.f_m(m) self.set_weights(irls=self.get_lp_weights(f_m)) class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): - """ - Base Class for sparse regularization on first spatial derivatives + r"""Sparse smoothness (blockiness) regularization. + + ``SparseSmoothness`` is used to recover models comprised of blocky structures. + The level of blockiness is controlled by the choice in `norm` within the regularization function; + with more blocky structures being recovered when a smaller norm is used. + Optionally, the `weights` argument can be used to supplied custom weighting to control + the degree of blockiness being enforced throughout different regions the model. + + See the *Notes* section below for a full mathematical description. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. + orientation : str {'x','y','z'} + The direction along which sparse smoothness is applied. + gradient_type : str {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total gradient or components of the gradient. + norm : float + The norm used in the regularization function. Must be within the interval [0, 2]. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. + If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + units : None, str + Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on + the :py:class:`~SimPEG.regularization.RegularizationMesh`. + + Notes + ----- + The regularization function for sparse smoothness (blockiness) is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg ( \frac{\partial}{\partial x} \Big [ m(r) - m_{ref}(r) \Big ] \bigg)^p \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` + is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` + and :math:`w(r)` are continuous variables as a function of location :math:`r`. + The parameter :math:`p \in [0,2]` is defined using the `norm` input argument, + where a smaller norm is used to recovery more blocky structures. + + For practical implementation within SimPEG, the regularization function and the aforementioned variables + are discretized onto a mesh. The discrete approximation of the regularization function is evaluated using a + weighted least-squares (IRLS) approach. For blockiness (sharp boundaries) along + the x-direction, the discrete regularization function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T W^T R^T R W G_x} (\mathbf{m} - \mathbf{m_{ref}}) + + where + + - :math:`\mathbf{m}` are the discrete model parameters, + - :math:`\mathbf{m_{ref}}` is a reference model (optional, and set using `reference_model`), + - :math:`\mathbf{G_x}` is partial cell gradient operator along the x-direction, + - :math:`\mathbf{W}` is the weighting matrix, and + - :math:`\mathbf{R}` is a model-dependent re-weighting matrix that is updated at every IRLS iteration. + + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + + where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; + optionally set using `weights`. :math:`\mathbf{A_{fc}}` averages from faces to cell centers + and :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions + when the regularization function is discretized to the mesh. + + The re-weighting matrix :math:`\mathbf{R}` is responsible for evaluating the proper norm in the regularization function. + :math:`\mathbf{R}` is a sparse diagonal matrix whose elements are recomputed at every IRLS iteration. For IRLS iteration :math:`k`, the + :math:`i^{th}` diagonal element is computed as follows: + + .. math:: + R_{ii}^{(k)} = \bigg [ \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} \bigg ]^{1/2} + + where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). """ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): @@ -164,8 +366,15 @@ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): super().__init__(mesh=mesh, orientation=orientation, **kwargs) def update_weights(self, m): - """ - Compute and store the irls weights. + r"""Compute and update the IRLS weights. + + The re-weighting matrix :math:`\mathbf{R}` ensure the specified norm is evaluated in the regularization function. + For a mathematically description of IRLS weights, see the *Notes* section in the :class:`SparseSmallness` class documentation. + + Parameters + ---------- + m : numpy.ndarray + The model. """ if self.gradient_type == "total": delta_m = self.mapping * self._delta_m(m) @@ -201,8 +410,12 @@ def update_weights(self, m): @property def gradient_type(self) -> str: - """ - Choice of gradient measure used in the irls weights + """Gradient measure used in the IRLS re-weighting. + + Returns + ------- + str in {"total", "components"} + Whether to re-weight using the total gradient or components of the gradient. """ return self._gradient_type @@ -218,29 +431,80 @@ def gradient_type(self, value: str): class Sparse(WeightedLeastSquares): - r""" - The regularization is: - - .. math:: + r"""Sparse norm weighted least squares regularization. - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top R^\top R - W(m-m_\text{ref})} + Construct a regularization for recovering compact and/or blocky structures + using a weighted sum of :class:`SparseSmallness` and :class:`SparseSmoothness` + regularization functions. - where the IRLS weight + See the *Notes* section below for a full mathematical description of the regularization. - .. math:: + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the model parameters are defined. + gradient_type : str {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total gradient or components of the gradient. + norms : (dim+1, ) array_like + The respective norms used for the sparse smallness, x-smoothness, (y-smoothness and z-smoothness) regularization function. + Must all be within the interval [0, 2]. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + active_cells : array_like of bool or int, optional + List of active cell indices, or a `mesh.n_cells` boolean array + describing active cells. + alpha_s : float, optional + Smallness weight + alpha_x, alpha_y, alpha_z : float or None, optional + First order smoothness weights for the respective dimensions. + `None` implies setting these weights using the `length_scale` + parameters. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + mapping : SimPEG.maps.IdentityMap, optional + A mapping to apply to the model before regularization. + reference_model : array_like, optional + Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + weights : None, array_like, or dict of array_like, optional + User defined weights. It is recommended to interact with weights using + the :py:meth:`~get_weights`, :py:meth:`~set_weights` methods. - R = \eta \text{diag} \left[\mathbf{r}_s \right]^{1/2} \ - r_{s_i} = {\Big( {({m_i}^{(k-1)})}^{2} + \epsilon^2 \Big)}^{p_s/2 - 1} + Notes + ----- + The model objective function :math:`\phi_m (m)` defined by the weighted sum of sparse smallness and smoothness + regularization functions is given by: - where k denotes the iteration number. So the derivative is straight forward: + .. math:: + \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) \Big [ m(r) - m_{ref}(r) \Big ]^{p_s} \, dv + + \sum_{i=x,y,z} \frac{\alpha_i}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial}{\partial \xi_i} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^{p_i} \, dv + + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model that may or may not be + included in the smoothness terms, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_i` is + the unit direction along :math:`i`. Constants :math:`\alpha_s` and :math:`\alpha_i` weight the respective + contributions of the smallness and first-order smoothness regularization functions. By our definition, + :math:`m(r)`, :math:`m_{ref}(r)` and :math:`w(r)` are continuous variables as a function of location :math:`r`. + + For practical implementation within SimPEG, the model objective function and the aforementioned variables + are discretized onto a mesh; set upon instantiation. The discrete approximation to the model objective function is given by: .. math:: + \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} \| \mathbf{R_s W_s} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \sum_{i=x,y,z} \frac{\alpha_i}{2} \| \mathbf{R_i W_i G_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + where - R(m) = \mathbf{W^\top R^\top R W (m-m_\text{ref})} + - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), + - :math:`\mathbf{m_{ref}}` is a reference model which may or may not be inclulded in the smoothess terms, + - :math:`\mathbf{G_i}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{W}` are weighting matrices, and + - :math:`\mathbf{R}` are the IRLS re-weighting matrices - The IRLS weights are re-computed after each beta solves using - :class:`~SimPEG.directives.Update_IRLS` within the inversion directives. + See the documentation for :class:`SparseSmallness` and :class:`SparseSmoothness` + for more details on how weighting matrices are constructed for each term. """ def __init__( From 849454eb78e808fa6f3935427af0046261ba69fe Mon Sep 17 00:00:00 2001 From: dccowan Date: Mon, 15 May 2023 20:47:42 -0700 Subject: [PATCH 075/455] more edits to least squares and sparse docstrings --- SimPEG/regularization/__init__.py | 6 +- SimPEG/regularization/base.py | 803 ++++++++++++++++++------------ SimPEG/regularization/sparse.py | 588 ++++++++++++++++------ 3 files changed, 926 insertions(+), 471 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index a1178c2827..a53b649d69 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -53,8 +53,10 @@ \alpha_y \! \int_\Omega \Bigg [ \frac{1}{2} w_y(r) \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv where :math:`w_s(r), w_x(r), w_y(r)` are user-defined weighting functions. -Discretized to a numerical grid (or mesh), the model becomes a discrete vector :math:`\mathbf{m}`. -And the aforementioned expression is approximated by: +For practical implementation within SimPEG, the regularization function and all its dependent +variables are discretized to a numerical grid (or mesh). The model is therefore defined as a +discrete set of model parameters :math:`\mathbf{m}`. +And the regularization function is approximated by: .. math:: \begin{align} diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index e55e7c0b99..b27759d7e5 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -18,25 +18,30 @@ class BaseRegularization(BaseObjectiveFunction): """Base class for least-squares regularization. - The ``BaseRegularization`` class defines properties and methods inherited by least-squares - regularization classes. It is not directly used to constrain the inversions. + The ``BaseRegularization`` class defines properties and methods inherited by + least-squares regularization classes. It is not directly used to constrain + the inversions. Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. - If ``None``, all cells are active. + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the quantity defined in the + regularization. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the + reference model is equal to the starting model for the inversion. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. """ @@ -83,9 +88,9 @@ def __init__( def active_cells(self) -> np.ndarray: """Active cells defined on the regularization mesh. - A boolean array defining the cells in the :py:class:`~SimPEG.regularization.RegularizationMesh` - that are active throughout the inversion. Inactive cells remain fixed and are defined according - to the starting model. + A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` + that are active throughout the inversion. Inactive cells remain fixed and are defined + according to the starting model. Returns ------- @@ -121,7 +126,7 @@ def active_cells(self, values: np.ndarray | None): @property def model(self) -> np.ndarray: - """The model associated with regularization. + """The model parameters. Returns ------- @@ -148,7 +153,8 @@ def mapping(self) -> maps.IdentityMap: Returns ------- SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + The mapping from the model parameters to the quantity defined on the + :py:class:`~.regularization.RegularizationMesh`. """ return self._mapping @@ -240,11 +246,12 @@ def reference_model(self, values: np.ndarray | float): def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. - Mesh on which the regularization is discretized. This is not the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. See :class:`.regularization.RegularizationMesh` Returns ------- - discretize.base.RegularizationMesh + .regularization.RegularizationMesh Mesh on which the regularization is discretized. """ return self._regularization_mesh @@ -354,18 +361,8 @@ def remove_weights(self, key): def W(self) -> np.ndarray: r"""Weighting matrix. - For a set of (n_cells, ) numpy arrays :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` - representing custom cell weights applied in the regularization, this method returns the cell weighting - matrix :math:`\mathbf{W}`, where: - - .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \, \mathbf{w_i} \bigg )^{1/2} \bigg ] - - The vector :math:`\mathbf{\tilde{v}}` accounts for cell volumes and dimensions - when the regularization function is discretized to the mesh. - - Weights are set using the `weights` property. For a comprehensive mathematical - description of the weighting matrix, see the *Notes* section for :class:`WeightedLeastSquares`. + Returns the weighting matrix for the discrete regularization function. For a comprehensive + description of the weighting matrix, the *Notes* section for the :class:`Smallness` class. Returns ------- @@ -460,7 +457,8 @@ def deriv2(self, m, v=None) -> csr_matrix: or the second-derivative multiplied by a given vector :math:`(\mathbf{v})` .. math:: - \bigg [ \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} \bigg ] \mathbf{v} + \bigg [ \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} + \bigg |_\mathbf{m} \bigg ] \mathbf{v} Parameters @@ -473,8 +471,9 @@ def deriv2(self, m, v=None) -> csr_matrix: Returns ------- (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the second-derivative of the regularization function for the model - provided is returned. If *v* is not ``None``, the second-derivative multiplied by the vector provided is returned. + If the input argument *v* is ``None``, the second-derivative of the regularization + function for the model provided is returned. If *v* is not ``None``, + the second-derivative multiplied by the vector provided is returned. """ f_m_deriv = self.f_m_deriv(m) @@ -489,62 +488,93 @@ class Smallness(BaseRegularization): ``Smallness`` regularization is used to ensure that differences between the model values in the recovered model and the reference model are small; - i.e. it preserves structures in the reference model. If the `reference_model` argument is not - used to set a reference model, the starting model will be set as the - reference model in the regularization by default. Optionally, the `weights` argument can be used - to supply custom weights to control the degree of smallness being enforced - throughout different regions the model. + i.e. it preserves structures in the reference model. If the reference model is + ``None``, the starting model will be set as the reference model in the + regularization by default. Optionally, custom cell weights can be included to + control the degree of smallness being enforced throughout different regions the model. - See the *Notes* section below for a full mathematical description. + See the *Notes* section below for a comprehensive description. Parameters ---------- - mesh : discretize.base.BaseMesh mesh - Mesh on which the regularization is discretized - shape : int - The number of model parameters - mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, .maps.IdentityMap + The mapping from the model parameters to the quantity defined in the regularization. + If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting + is set as the reference model by default. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. Notes ----- The regularization function for smallness is defined as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \big [ m(r) - m_{ref}(r) \big ]^2 \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` - and :math:`w(r)` are continuous variables as a function of location :math:`r`. + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` + is a user-defined weighting function. - For practical implementation within SimPEG, the regularization function and the aforementioned variables - are discretized onto a mesh (set upon instantiation). The discrete approximation to the regularization function is given by: + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. The general form of the discrete regularization + function is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T - \mathbf{W}^T \mathbf{W} (\mathbf{m} - \mathbf{m_{ref}}) + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 + + where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account + for cell dimensions in the discretization and apply user-defined weighting. In terms of linear + operators, the discrete regularization function can be expressed as: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where - - :math:`\mathbf{m}` are the discrete model parameters (model), - - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), and + - :math:`\mathbf{m}^{(ref)}` is a reference model (set using `reference_model`), and - :math:`\mathbf{W}` is the weighting matrix. - The weighting matrix is given by: + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the discrete regularization function + is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + + where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes + and dimensions when the regularization function is discretized to the mesh. + The weights are implemented using a weighting matrix given by: .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) + ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; - optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions - when the regularization function is discretized to the mesh. """ _multiplier_pair = "alpha_s" @@ -559,9 +589,9 @@ def f_m(self, m) -> np.ndarray: For ``Smallness`` regularization, the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - where :math:`\mathbf{m}` are the descrete model parameters and :math:`\mathbf{m_{ref}}` + where :math:`\mathbf{m}` are the descrete model parameters and :math:`\mathbf{m}^{(ref)}` is a reference model. For a more detailed description, see the *Notes* section below. Parameters @@ -579,33 +609,35 @@ def f_m(self, m) -> np.ndarray: The discretized form of the smallness regularization function is expressed as: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` - is the reference model, and :math:`\mathbf{W}` is the weighting matrix. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 - For a more comprehensive description of the regularization function, see the *Notes* section - with documentation for the :class:`Smallness` class. + For a more comprehensive description of the regularization function, + see the *Notes* section with documentation for the :class:`Smallness` class. """ return self.mapping * self._delta_m(m) def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``Smallness`` regularization, the derivative of the least-squares regularization kernel - with respect to the model is given by: + For ``Smallness`` regularization, the derivative of the least-squares regularization + kernel with respect to the model is given by: .. math:: - \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{I} + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} where :math:`\mathbf{I}` is the identity matrix: For a more detailed description, see the *Notes* section below. @@ -622,30 +654,32 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the ``Smallness`` regularization function is expressed as: + The discretized form of the smallness regularization function is expressed as: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{W^T W} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` - is the reference model, and :math:`\mathbf{W}` is the weighting matrix. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m_{ref}} + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 Thus, the derivate with respect to the model is: .. math:: - \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{I} + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. - For a more comprehensive description of the regularization function, see the *Notes* section + where :math:`\mathbf{I}` is the identity matrix. For a more comprehensive + description of the regularization function, see the *Notes* section with documentation for the :class:`Smallness` class. """ return self.mapping.deriv(self._delta_m(m)) @@ -654,69 +688,122 @@ def f_m_deriv(self, m) -> csr_matrix: class SmoothnessFirstOrder(BaseRegularization): r"""First-order smoothness least-squares regularization. - ``SmoothnessFirstOrder`` regularization is used to ensure that values in the recovered model - are smooth along a specified direction. When the `reference_model` argument used to set a reference model, - the regularization preserves gradients/interfaces within the reference model along the direction - specified by the `orientation` argument. Optionally, the `weights` argument can be used - to supply custom weights to control the degree of smoothness being enforced - throughout different regions the model. + ``SmoothnessFirstOrder`` regularization is used to ensure that values in the recovered + model are smooth along a specified direction. When a reference model is included, + the regularization preserves gradients/interfaces within the reference model along + the direction specified (x, y or z). Optionally, custom cell weights can be used + to control the degree of smoothness being enforced throughout different regions + the model. - See the *Notes* section below for a full mathematical description. + See the *Notes* section below for a comprehensive description. Parameters ---------- mesh : discretize.base.BaseMesh mesh The mesh on which the regularization is discretized. - orientation : str {'x', 'y', 'z'} - The direction along which smoothness is enforced. Default = 'x'. - reference_model_in_smooth : bool - Whether the reference model is included in the smoothness regularization. If ``False``, it is equivalent to setting the reference model to 0. - shape : int - The number of model parameters - mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + orientation : {'x', 'y', 'z'} + The direction along which smoothness is enforced. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, .maps.IdentityMap + The mapping from the model parameters to the quantity defined in the regularization. + If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting + is set as the reference model by default. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave differently + depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. Notes ----- The regularization function for first-order smoothness along the x-direction is given by: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w (r) \Bigg ( \frac{\partial}{\partial x} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` - and :math:`w(r)` are continuous variables as a function of location :math:`r`. + where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. The general form of the discrete regularization + function is given by: - For practical implementation within SimPEG, the regularization function and the aforementioned variables - are discretized onto a mesh (set upon instantiation). The discrete approximation to the regularization function is given by: + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 + + where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account + for cell dimensions in the discretization and apply user-defined weighting. In terms of linear + operators, the discrete regularization function can be expressed as: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x}^T - \mathbf{W}^T \mathbf{W} \mathbf{G_x} (\mathbf{m} - \mathbf{m_{ref}}) + \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x m } \, \Big \|^2 where - - :math:`\mathbf{m}` are the discrete model parameters (model), - - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), - - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and + - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, and - :math:`\mathbf{W}` is the weighting matrix. + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, + :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by + including the reference model the smoothness regularization function. In this case, + the regularization function is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg [ \frac{\partial}{\partial x} \big ( m - m^{(ref)} \big ) \bigg ]^2 \, dv + + When discretized onto a mesh, the regularization function is approximated by: - The weighting matrix is given by: + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the discrete regularization function + is given by: .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \mathbf{A_{fc}}^T \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + \mathbf{\tilde{w}} = \mathbf{A_{cx}} + \bigg [ \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} \bigg ] + + where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes + and dimensions when the regularization function is discretized to the mesh. + :math:`\mathbf{A_{cfx}}` projects cell variables to x-faces (where the x-derivative lives). + The weights are implemented using a weighting matrix given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) + ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = SmoothnessFirstOrder(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2) - where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; - optionally set using `weights`. :math:`\mathbf{A_{fc}}` averages from faces to cell centers - and :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions - when the regularization function is discretized to the mesh. """ def __init__( @@ -760,14 +847,15 @@ def _weights_shapes(self): def cell_gradient(self): """Partial cell gradient operator. - Returns the partial gradient operator which takes the derivative along the orientation - where smoothness is being enforced. For smoothness along the x-direction, the resulting operator would - map from cell centers to x-faces. + Returns the partial gradient operator which takes the derivative along the + orientation where smoothness is being enforced. For smoothness along the + x-direction, the resulting operator would map from cell centers to x-faces. Returns ------- scipy.sparse.csr_matrix - Partial cell gradient operator defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Partial cell gradient operator defined on the + :py:class:`.regularization.RegularizationMesh`. """ if getattr(self, "_cell_gradient", None) is None: self._cell_gradient = getattr( @@ -801,14 +889,16 @@ def _multiplier_pair(self): def f_m(self, m): r"""Evaluate least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization in the x-direction, the least-squares regularization kernel is given by: + For ``SmoothnessFirstOrder`` regularization in the x-direction, + the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)}} \big ] - where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), - :math:`\mathbf{m}` are the descrite model parameters and :math:`\mathbf{m_{ref}}` is the reference model. - For a more detailed, description, see the notes. + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction + (i.e. x-derivative), :math:`\mathbf{m}` are the descrite model parameters and + :math:`\mathbf{m}^{(ref)}` is the reference model. For a more detailed description + see the notes. Parameters ---------- @@ -822,24 +912,29 @@ def f_m(self, m): Notes ----- - The discretized form of the first order smoothness regularization function along the x-direction is expressed as: + The discretized form of the first order smoothness regularization function along + the x-direction is expressed as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x^T W^T W G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` - is a reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along - the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. - Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial + cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + such that .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x \, f_m} \Big \|^2 - For a more comprehensive description of the regularization function, see the *Notes* section - with documentation for the :class:`SmoothnessFirstOrder` class. + For a more comprehensive description of the regularization function, see the + *Notes* section of the documentation for the :class:`SmoothnessFirstOrder` class. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -853,13 +948,14 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization, the least-squares regularization kernel is given by: + For ``SmoothnessFirstOrder`` regularization, the least-squares regularization kernel + is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - And thus, the derivative with respect to the model is the x-derivative operator :math:`\mathbf{G_x}`. - For a more detailed, description, see the notes. + And thus, the derivative with respect to the model is the x-derivative + operator :math:`\mathbf{G_x}`. For a more detailed, description, see the notes. Parameters ---------- @@ -873,29 +969,35 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized regularization function for smoothness along the x-direction is expressed as: + The discretized form of the first order smoothness regularization function along + the x-direction is expressed as: + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial + cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{G_x^T W^T W G_x} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m_{ref}}` - is a reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along - the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. - Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: + such that .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W G_x} \, \mathbf{f_m} \Big \|^2 The derivate with respect to the model is therefore: .. math:: - \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{G_x} + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} - For a more comprehensive description of the regularization function, see the *Notes* section - with documentation for the :class:`SmoothnessFirstOrder` class. + For a more comprehensive description of the regularization function, see the *Notes* + section of the documentation for the :class:`SmoothnessFirstOrder` class. """ return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) @@ -903,24 +1005,14 @@ def f_m_deriv(self, m) -> csr_matrix: def W(self): r"""Weighting matrix. - A sparse, diagonal weighting matrix for all weights associated with the - regularization object. This includes default weights that are set when the regularization object - is instantiated (e.g. cell volumes and length scales corresponding to the - difference operator), as well as any user-defined weights. - - The weighting matrix is given by: - - .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \mathbf{A_{fc}}^T \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] - - The vector :math:`\mathbf{\tilde{v}}` accounts for cell volumes and dimensions - when the regularization function is discretized to the mesh. - And :math:`\mathbf{A_{fc}}` averages from faces to cell centers. + Returns the weighting matrix for the discrete regularization function. For a comprehensive + description of the weighting matrix, the *Notes* section for the + :class:`SmoothnessFirstOrder` class. Returns ------- scipy.sparse.csr_matrix - Weighting matrix. + The weighting matrix applied in the regularization. """ if getattr(self, "_W", None) is None: average_cell_2_face = getattr( @@ -947,8 +1039,8 @@ def orientation(self): Returns ------- - str {'x', 'y', 'z'} - The direction along which smoothness is enforced. Default = 'x'. + str + The direction along which smoothness is enforced. On of {'x','y','z'} """ return self._orientation @@ -957,80 +1049,130 @@ def orientation(self): class SmoothnessSecondOrder(SmoothnessFirstOrder): r"""Second-order smoothness (flatness) least-squares regularization. - ``SmoothnessSecondOrder`` regularization is used to ensure that values in the recovered model - have small second-order spatial derivatives along a specified direction. When `reference_model` is used - to provide reference model, the regularization preserves second-order spatial derivatives within the - reference model along the direction defined by the `orientation` argument. - Optionally, the `weights` argument can be used to supply custom weights to control the degree of - smoothness being enforced throughout different regions the model. + ``SmoothnessSecondOrder`` regularization is used to ensure that values in the recovered + model have small second-order spatial derivatives. When a reference model is included, + the regularization preserves second-order smoothness within the reference model along + the direction specified (x, y or z). Optionally, custom cell weights can be used + to control the degree of smoothness being enforced throughout different regions + the model. - See the *Notes* section below for a full mathematical description. + See the *Notes* section below for a comprehensive description. Parameters ---------- mesh : discretize.base.BaseMesh mesh The mesh on which the regularization is discretized. - orientation : str {'x', 'y', 'z'} - The direction along which smoothness is enforced. Default = 'x'. - reference_model_in_smooth : bool - Whether the reference model is included in the smoothness regularization. Default = ``False``. - shape : int - The number of model parameters - mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + orientation : {'x', 'y', 'z'} + The direction along which smoothness is enforced. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, .maps.IdentityMap + The mapping from the model parameters to the quantity defined in the regularization. + If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting + is set as the reference model by default. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave differently + depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. Notes ----- The regularization function for second-order smoothness along the x-direction is given by: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w (r) \Bigg ( \frac{\partial^2}{\partial x^2} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv + + where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. The general form of the discrete regularization + function is given by: - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` - and :math:`w(r)` are continuous variables as a function of location :math:`r`. + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - For practical implementation, the regularization function and the aforementioned variables - are discretized onto a mesh; which is set upon instantiation. The discrete approximation to the regularization function is given by: + where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account + for cell dimensions in the discretization and apply user-defined weighting. In terms of linear + operators, the discrete regularization function can be expressed as: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{L_x}^T - \mathbf{W}^T \mathbf{W} \mathbf{L_x} (\mathbf{m} - \mathbf{m_{ref}}) + \gamma (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 where - - :math:`\mathbf{m}` are the discrete model parameters (model), - - :math:`\mathbf{m_{ref}}` is a reference model (set using `reference_model`), - :math:`\mathbf{L_x}` is a second-order derivative operator with respect to :math:`x`, and - :math:`\mathbf{W}` is the weighting matrix. + + **Reference model in smoothness:** + + Second-order derivatives within a reference model :math:`m^{(ref)}` can be preserved by + including the reference model the smoothness regularization function. In this case, + the regularization function is given by: + + .. math:: + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg [ \frac{\partial^2}{\partial x^2} \Big ( m - m^{(ref)} \Big ) \bigg ]^2 \, dv + + When discretized onto a mesh, the regularization function is approximated by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. - The weighting matrix is given by: + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the discrete regularization function + is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + + where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes + and dimensions when the regularization function is discretized to the mesh. + The weights are implemented using a weighting matrix given by: .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) + ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; - optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions - when the regularization function is discretized to the mesh. """ def f_m(self, m): r"""Evaluate least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization in the x-direction, the least-squares regularization kernel is given by: + For ``SmoothnessSecondOrder`` regularization in the x-direction, + the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator. - For a more detailed, description, see the notes. + For a more detailed description, see the notes. Parameters ---------- @@ -1044,25 +1186,30 @@ def f_m(self, m): Notes ----- - The discretized form of the second-order smoothness regularization function along the x-direction is expressed as: + The discretized form of the second order smoothness regularization function along + the x-direction is expressed as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{L_x^T W^T W L_x} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, - :math:`\mathbf{m}` are the dicrete model parameters, - :math:`\mathbf{m_{ref}}` is a reference model and :math:`\mathbf{W}` is a weighting - matrix that applies user-defined weights and accounts for cell dimensions in the integration. - Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, where :math:`\mathbf{L_x}` is the + discrete second order x-derivative operator, and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + such that .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W L_x} \, \mathbf{f_m} \Big \|^2 - For a more comprehensive description of the regularization function, see the *Notes* section - with documentation for the :class:`SmoothnessSecondOrder` class. + For a more comprehensive description of the regularization function, see the *Notes* + section with documentation for the :class:`SmoothnessSecondOrder` class. """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -1079,15 +1226,18 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization, the least-squares regularization kernel is given by: + For ``SmoothnessSecondOrder`` regularization, the least-squares regularization + kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x}^T \big ( \mathbf{m - m_{ref}} \big ) + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - And thus, the derivative with respect to the model is + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, + :math:`\mathbf{m}` are the dicrete model parameters, :math:`\mathbf{m}^{(ref)}` + is a reference model. And thus, the derivative with respect to the model is .. math:: - \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{L_x} + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} For a more detailed, description, see the notes. @@ -1103,30 +1253,35 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the smoothness regularization function in along the x-direction is expressed as: + The discretized form of the second order smoothness regularization function along + the x-direction is expressed as: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &= \frac{1}{2} \big ( \mathbf{m - m_{ref}} \big )^T \mathbf{L_x^T W^T W L_x} \big ( \mathbf{m - m_{ref}} \big ) \\ - &= \frac{1}{2} \mathbf{f_m}^T \mathbf{W^T W} \, \mathbf{f_m} - \end{align} + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, - :math:`\mathbf{m}` are the set of discrete model parameters, - :math:`\mathbf{m_{ref}}` is a reference model and :math:`\mathbf{W}` is a weighting - matrix that applies user-defined weights and accounts for cell dimensions in the integration. - Thus the least-squares regularization kernel :math:`\mathbf{f_m}` is defined as: + where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model, where :math:`\mathbf{L_x}` is the + discrete second order x-derivative operator, and :math:`\mathbf{W}` is + the weighting matrix. We define the least-squares regularization kernel + :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + such that .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big ( \mathbf{m - m_{ref}} \big ) + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W L_x} \, \mathbf{f_m} \Big \|^2 The derivate with respect to the model is: .. math:: - \nabla_\mathbf{m} \mathbf{f_m} = \mathbf{L_x} + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - For a more comprehensive description of the regularization function, see the *Notes* section - with documentation for the :class:`SmoothnessSecondOrder` class. + For a more comprehensive description of the regularization function, see the + *Notes* section with documentation for the :class:`SmoothnessSecondOrder` class. """ return ( self.cell_gradient.T @@ -1162,89 +1317,127 @@ class WeightedLeastSquares(ComboObjectiveFunction): and/or :class:`SmoothnessSecondOrder` (optional) least-squares regularization functions. ``Smallness`` regularization is used to ensure that values in the recovered model, or differences between the recovered model and a reference model, are not overly - large in magnitude. ``Smoothness`` regularizations are used to ensure that values in the recovered model - are smooth along specified directions. When `reference_in_smooth` is used to include the reference model - in the smoothness terms, the inversion preserves gradients/interfaces within the reference model. - the `weights` argument can be used to supply custom weights to control the degree of - smallness and smoothness being enforced throughout different regions the model. - - See the *Notes* section below for a full mathematical description of the regularization. - - By default, second-order smoothness is not included in the regularization; i.e. input parameters - `alpha_xx, alpha_yy, alpha_zz = 0`. And the reference model is not included in any smoothness terms; - i.e. `reference_model_in_smooth` is ``False``. The user may set the weighting constants - `alpha` directly, or indirectly using length scales such that: + large in magnitude. ``Smoothness`` regularizations are used to ensure that values in the + recovered model are smooth along specified directions. When `reference_in_smooth` is + used to include the reference model in the smoothness terms, the inversion preserves + gradients/interfaces within the reference model. The `weights` argument can be used + to supply custom weights to control the degree of smallness and smoothness being + enforced throughout different regions the model. - >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 - - and - - >>> alpha_xx = (length_scale_x * min(mesh.edge_lengths)) ** 4 + See the *Notes* section below for a comprehensive description. Parameters ---------- - mesh : discretize.base.BaseMesh - The mesh on which the regularization is discretized. This is used - for constructing difference operators for the smoothness terms. - active_cells : array_like of bool or int, optional - List of active cell indices, or a `mesh.n_cells` boolean array - describing active cells. + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the + regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the + reference model is equal to the starting model for the inversion. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. alpha_s : float, optional - Smallness weight + Scaling constant for the smallness regularization term. alpha_x, alpha_y, alpha_z : float or None, optional - First order smoothness weights for the respective dimensions. - `None` implies setting these weights using the `length_scale` - parameters. + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. alpha_xx, alpha_yy, alpha_zz : 0, float - Second order smoothness weights for the respective dimensions. By default, second order - smoothness is unused in the regularization. + Scaling constants for the second order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional - A mapping to apply to the model before regularization. - reference_model : array_like, optional - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict of array_like, optional - User defined weights. It is recommended to interact with weights using - the :py:meth:`~get_weights`, :py:meth:`~set_weights` methods. Notes ----- - The model objective function :math:`\phi_m (m)` defined by the weighted sum of smallness and smoothness + The model objective function :math:`\phi_m (m)` defined by the weighted sum of + :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` regularization functions is given by: .. math:: - \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) \Big [ m(r) - m_{ref}(r) \Big ]^2 \, dv \\ - &+ \sum_{i=x,y,z} \frac{\alpha_i}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial}{\partial \xi_i} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv \\ - &+ \sum_{i=x,y,z} \frac{\alpha_{ii}}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial^2}{\partial \xi_i^2} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^2 \, dv - - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model that may or may not be - included in each term, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_i` is - the unit direction along :math:`i`. Constants :math:`\alpha_s`, :math:`\alpha_i` and - :math:`\alpha_{ii}` (optional) weight the respective contributions of the smallness, first-order smoothness, - and second-order smoothness regularization functions. By our definition, - :math:`m(r)`, :math:`m_{ref}(r)` and :math:`w(r)` are continuous variables as a function of location :math:`r`. - - For practical implementation, the model objective function and the aforementioned variables - are discretized onto a mesh; set upon instantiation. The discrete approximation to the model objective function is given by: + \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) + \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^2 \, dv \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) + \bigg [ \frac{\partial^2 m}{\partial \xi_j^2} \bigg ]^2 \, dv + \;\;\;\;\; ( \leftarrow \, optional ) + + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` + is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. + Constants :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` weight the respective + contributions of the smallness and smoothness regularization functions. + + For implementation within SimPEG, regularization functions and their variables + are discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose + discrete representation is given by :math:`\mathbf{x}`: + + .. math:: + \int_\Omega w(r) \big [ x(r) \big ]^2 \, dv = \sum_i \tilde{w}_i \, | x_i |^2 + + :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions in + the discretization and apply user-defined weighting. + + For ``Sparse`` regularization, the discretized model objective function is given by: .. math:: - \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} \| \mathbf{W_s} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ - &+ \sum_{i=x,y,z} \frac{\alpha_i}{2} \| \mathbf{W_i G_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 \\ - &+ \sum_{ii=x,y,z} \frac{\alpha_{ii}}{2} \| \mathbf{W_i L_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} + \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\; ( \leftarrow \, optional ) where - - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), - - :math:`\mathbf{m_{ref}}` is a reference model which may or may not be inclulded in the smoothess terms, - - :math:`\mathbf{G_i}` are partial cell gradients operators along x, y and z, - - :math:`\mathbf{L_i}` are second-order derivative operators for x, y and z, and + - :math:`\mathbf{G_j}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_j}` are second-order derivative operators with respect to x, y and z, - :math:`\mathbf{W}` are weighting matrices. - See the documentation for :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` - for more details on how the weighting matrices are constructed. + **Alpha weighting parameters:** + + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by + including the reference model the smoothness regularization functions. In this case, + the regularization function is given by: + + .. math:: + \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) + \Big [ m(r) - m_{ref}(r) \Big ]^2 \, dv \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + \bigg [ \frac{\partial}{\partial \xi_j} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) + \bigg [ \frac{\partial^2}{\partial \xi_j^2} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 + \;\;\;\;\; ( \leftarrow \, optional ) + + When discretized onto a mesh, the regularization function is approximated by: + + .. math:: + \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} + \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \;\;\;\;\; ( \leftarrow \, optional ) + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. """ _model = None diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 91f1c4a523..b0efa538ca 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -31,25 +31,31 @@ class BaseSparse(BaseRegularization): Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - norm : float - The norm used in the regularization function. Must be between within the interval [0, 2]. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. - If ``None``, all cells are active. + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the quantity defined in the + regularization. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the + reference model is equal to the starting model for the inversion. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + norm : float + The norm used in the regularization function. Must be between within the interval [0, 2]. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + """ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): @@ -179,79 +185,142 @@ class SparseSmallness(BaseSparse, Smallness): r"""Sparse smallness (compactness) regularization. ``SparseSmallness`` is used to recover models comprised of compact structures. - The level of compactness is controlled by the `norm` within the regularization function; - with more compact structures being recovered when a smaller norm is used. - Optionally, the `weights` argument can be used to supply custom weights to control the degree of compactness being enforced - throughout different regions the model. + The level of compactness is controlled by the norm within the regularization + function; with more compact structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of compactness + being enforced throughout different regions the model. - See the *Notes* section below for a full mathematical description. + See the *Notes* section below for a comprehensive description. Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. norm : float - The norm used in the regularization function. Must be within the interval [0, 2]. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. + The norm used in the regularization function. Must be within theinterval [0, 2]. active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. - If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + Array of bool defining the set of :py:class:`.regularization.RegularizationMesh` cells + that are active in the inversion. If ``None``, all cells are active. + mapping : None, .maps.IdentityMap + The mapping from the model parameters to the quantity defined in the regularization. + If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting + is set as the reference model by default. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. Notes ----- The regularization function for sparse smallness (compactness) is given by: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \big [ m(r) - m_{ref}(r) \big ]^p \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \Big | m(r) - m^{(ref)}(r) \Big |^p \, dv - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` - and :math:`w(r)` are continuous variables as a function of location :math:`r`. - The parameter :math:`p \in [0,2]` is defined using the `norm` input argument, - where a smaller norm is used to recovery more compact structures. + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` + is a user-defined weighting function. - For practical implementation, the regularization function and the aforementioned variables - are discretized onto a mesh. The discrete approximation to the regularization function is given by: + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. The general form of the discrete regularization + function is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{W^T R^T R W} (\mathbf{m} - \mathbf{m_{ref}}) + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^p + + where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that accounts + for cell dimensions in the discretization and applies user-defined weighting. + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \gamma \big (\mathbf{m}^{(k)} \big ) + = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^p + \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big [ m_i^{(k)} - m_i^{(ref)} \Big ]^p + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( m_i^{(k-1)} - m_i^{(ref)} \Big )^2 + + \epsilon^2 \; \bigg ]^{p/2 - 1} + + and :math:`\epsilon` is a small constant added for stability. For the set of model parameters + :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS + iteration :math:`k` can be expressed as follows: + + .. math:: + \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \bigg \| \, + \mathbf{W \, R} \, \Big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \Big ] \bigg \|^2 where - - :math:`\mathbf{m}` are the discrete model parameters, - - :math:`\mathbf{m_{ref}}` is a reference model (optional, set using `reference_model`), - - :math:`\mathbf{W}` is the weighting matrix, and - - :math:`\mathbf{R}` is a model-dependent re-weighting matrix that is updated at every IRLS iteration. + - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, + - :math:`\mathbf{m}^{(ref)}` is a reference model (optional, set with `reference_model`), + - :math:`\mathbf{R}` is the IRLS re-weighting matrix, and + - :math:`\mathbf{W}` is the weighting matrix. + + **IRLS weights and the re-weighting matrix:** + + The IRLS weights are model-dependent and must be computed at every IRLS iteration. + For iteration :math:`k`, the IRLS weights :math:`\mathbf{w_r}` are updated internally + using the previous model via: - The weighting matrix is given by: + .. math:: + \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) + \bigg [ \Big ( \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big )^2 + \epsilon^2 \bigg ]^{p/2 - 1} + + where :math:`\epsilon` is a small constant added for stability of the algorithm + (set using `irls_threshold`). :math:`\lambda (\mathbf{m})` is an optional scaling + constant (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse + norm has roughly the same magnitude as the equivalent 2-norm for the same model. + The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, + where .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{v} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + \mathbf{R} = \textrm{diag} \Big ( \mathbf{w_r}^{1/2} \Big ) - where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; - optionally set using `weights`. And :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions - when the regularization function is discretized to the mesh. + **Custom weights and the weighting matrix:** - The re-weighting matrix :math:`\mathbf{R}` is responsible for evaluating the proper norm in the regularization function. - :math:`\mathbf{R}` is a sparse diagonal matrix whose elements are recomputed at every IRLS iteration. For IRLS iteration :math:`k`, the - :math:`i^{th}` diagonal element is computed as follows: + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the discrete regularization function + is given by: .. math:: - R_{ii}^{(k)} = \bigg [ \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} \bigg ]^{1/2} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + + where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes + and dimensions when the regularization function is discretized to the mesh. + The weights are implemented using a weighting matrix given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) + ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). """ _multiplier_pair = "alpha_s" @@ -259,8 +328,10 @@ class SparseSmallness(BaseSparse, Smallness): def update_weights(self, m): r"""Compute and update the IRLS weights. - The re-weighting matrix :math:`\mathbf{R}` ensure the specified norm is evaluated in the regularization function. - For a mathematically description of IRLS weights, see the *Notes* section in the :class:`SparseSmallness` class documentation. + The weights used to construct the re-weighting matrix :math:`\mathbf{R}` for + sparse-norm inversion are model-dependent and must be updated at every IRLS iteration. + This method recomputes and stores the weights. For a comprehensive description, + see the *Notes* section in the :class:`SparseSmallness` class documentation. Parameters ---------- @@ -275,87 +346,184 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): r"""Sparse smoothness (blockiness) regularization. ``SparseSmoothness`` is used to recover models comprised of blocky structures. - The level of blockiness is controlled by the choice in `norm` within the regularization function; + The level of blockiness is controlled by the choice in norm within the regularization function; with more blocky structures being recovered when a smaller norm is used. - Optionally, the `weights` argument can be used to supplied custom weighting to control - the degree of blockiness being enforced throughout different regions the model. + Optionally, custom cell weights can be included to control the degree of blockiness being + enforced throughout different regions the model. - See the *Notes* section below for a full mathematical description. + See the *Notes* section below for a comprehensive description. Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - orientation : str {'x','y','z'} + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + orientation : {'x','y','z'} The direction along which sparse smoothness is applied. - gradient_type : str {"total", "component"} - Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total gradient or components of the gradient. norm : float - The norm used in the regularization function. Must be within the interval [0, 2]. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. + The norm used in the regularization function. Must be within theinterval [0, 2]. active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~SimPEG.regularization.RegularizationMesh` cells that are active in the inversion. - If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, .maps.IdentityMap + The mapping from the model parameters to the quantity defined in the regularization. + If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting + is set as the reference model by default. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. units : None, str - Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on - the :py:class:`~SimPEG.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total + gradient or components of the gradient. Notes ----- - The regularization function for sparse smoothness (blockiness) is given by: + The regularization function for sparse smoothness (blockiness) along the x-direction + is given by: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg ( \frac{\partial}{\partial x} \Big [ m(r) - m_{ref}(r) \Big ] \bigg)^p \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg | \frac{\partial m}{\partial x} \bigg |^p \, dv - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. By this definition, :math:`m(r)`, :math:`m_{ref}(r)` - and :math:`w(r)` are continuous variables as a function of location :math:`r`. - The parameter :math:`p \in [0,2]` is defined using the `norm` input argument, - where a smaller norm is used to recovery more blocky structures. + where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + The parameter :math:`p \in [0,2]` defines the `norm`, where a smaller norm is used to + recovery structures with sharper boundaries. - For practical implementation within SimPEG, the regularization function and the aforementioned variables - are discretized onto a mesh. The discrete approximation of the regularization function is evaluated using a - weighted least-squares (IRLS) approach. For blockiness (sharp boundaries) along - the x-direction, the discrete regularization function is given by: + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. The general form of the discrete regularization + function is given by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^p + + where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account + for cell dimensions in the discretization and apply user-defined weighting. + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big ( \mathbf{m - m_{ref}})^T \mathbf{G_x^T W^T R^T R W G_x} (\mathbf{m} - \mathbf{m_{ref}}) + \gamma \big (\mathbf{m}^{(k)} \big ) + = \frac{1}{2} \sum_i + \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^p + \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + \Bigg ( \, \frac{\partial \tilde{m}_i^{(k)}}{\partial x} \Bigg )^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial m_i^{(k-1)}}{\partial x} \Bigg )^2 + + \epsilon^2 \; \Bigg ]^{p/2 - 1} + + and :math:`\epsilon` is a small constant added for stability. For the set of model parameters + :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS + iteration :math:`k` can be expressed as follows: + + .. math:: + \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W \, R \, G_x} \, \mathbf{m}^{(k)} \Big \|^2 where - - :math:`\mathbf{m}` are the discrete model parameters, - - :math:`\mathbf{m_{ref}}` is a reference model (optional, and set using `reference_model`), - - :math:`\mathbf{G_x}` is partial cell gradient operator along the x-direction, - - :math:`\mathbf{W}` is the weighting matrix, and - - :math:`\mathbf{R}` is a model-dependent re-weighting matrix that is updated at every IRLS iteration. + - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, + - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), + - :math:`\mathbf{R}` is the IRLS re-weighting matrix, and + - :math:`\mathbf{W}` is the weighting matrix. + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, :math:`\mathbf{R}` and + :math:`\mathbf{W}` are operators that act on variables living on x-faces. + + **IRLS weights and the re-weighting matrix:** + + At every IRLS iteration, the IRLS weights :math:`\mathbf{w_r}` are recomputed internally + using the previous model via: - The weighting matrix is given by: + .. math:: + \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) + \Bigg [ \bigg ( \mathbf{G_x \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \bigg )^2 + + \epsilon^2 \bigg ]^{p/2 - 1} + + where :math:`\epsilon` is a small constant added for stability of the algorithm + (set using `irls_threshold`) and :math:`\lambda (\mathbf{m})` is an optional scaling constant + (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse + norm has roughly the same magnitude as the equivalent 2-norm for the same model. + The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where .. math:: - \mathbf{W} = \textrm{diag} \bigg [ \bigg ( \mathbf{\tilde{v}} \odot \prod_i \mathbf{w_i} \bigg )^{1/2} \bigg ] + \mathbf{R} = \textrm{diag} \Big ( \mathbf{w_r}^{1/2} \Big ) - where :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` represents a set of custom cell weights; - optionally set using `weights`. :math:`\mathbf{A_{fc}}` averages from faces to cell centers - and :math:`\mathbf{\tilde{v}}` accounts for all cell volumes and dimensions - when the regularization function is discretized to the mesh. + **Reference model in smoothness:** - The re-weighting matrix :math:`\mathbf{R}` is responsible for evaluating the proper norm in the regularization function. - :math:`\mathbf{R}` is a sparse diagonal matrix whose elements are recomputed at every IRLS iteration. For IRLS iteration :math:`k`, the - :math:`i^{th}` diagonal element is computed as follows: + Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by + including the reference model the smoothness regularization function. In this case, + the regularization function is given by: .. math:: - R_{ii}^{(k)} = \bigg [ \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} \bigg ]^{1/2} + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \bigg [ \frac{\partial}{\partial x} \Big ( m - m^{(ref)} \Big ) \bigg ]^p \, dv + + When discretized onto a mesh, the regularization function is approximated by: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \bigg \| \mathbf{W R G_x} + \Big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \Big ] \bigg \|^2 + + where the IRLS weights are given by: + + .. math:: + \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) + \Bigg [ \bigg ( \mathbf{G_x} \Big [ \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big ] \bigg )^2 + + \epsilon^2 \bigg ]^{p/2 - 1} + + This functionality is used by setting :math:`\mathbf{m^{(ref)}}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the discrete regularization function + is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{A_{cx}} + \bigg [ \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} \bigg ] + + where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes + and dimensions when the regularization function is discretized to the mesh. + :math:`\mathbf{A_{cfx}}` projects cell variables to x-faces (where the x-derivative lives). + The weights are implemented using a weighting matrix given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) + ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). """ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): @@ -368,8 +536,10 @@ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): def update_weights(self, m): r"""Compute and update the IRLS weights. - The re-weighting matrix :math:`\mathbf{R}` ensure the specified norm is evaluated in the regularization function. - For a mathematically description of IRLS weights, see the *Notes* section in the :class:`SparseSmallness` class documentation. + The weights used to construct the re-weighting matrix :math:`\mathbf{R}` for + sparse-norm inversion are model-dependent and must be updated at every IRLS iteration. + This method recomputes and stores the weights. For a comprehensive description, + see the *Notes* section in the :class:`SparseSmoothness` class documentation. Parameters ---------- @@ -435,65 +605,101 @@ class Sparse(WeightedLeastSquares): Construct a regularization for recovering compact and/or blocky structures using a weighted sum of :class:`SparseSmallness` and :class:`SparseSmoothness` - regularization functions. + regularization functions. The level of compactness and blockiness is + controlled by the norms within the respective regularization functions; + with more sparse structures (compact and/or blocky) being recovered when smaller + norms are used. Optionally, custom cell weights can be applied to control + the degree of sparseness being enforced throughout different regions the model. - See the *Notes* section below for a full mathematical description of the regularization. + See the *Notes* section below for a comprehensive description. Parameters ---------- - mesh : discretize.base.BaseMesh - Mesh on which the model parameters are defined. - gradient_type : str {"total", "component"} - Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total gradient or components of the gradient. - norms : (dim+1, ) array_like - The respective norms used for the sparse smallness, x-smoothness, (y-smoothness and z-smoothness) regularization function. - Must all be within the interval [0, 2]. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. - active_cells : array_like of bool or int, optional - List of active cell indices, or a `mesh.n_cells` boolean array - describing active cells. + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, numpy.ndarray of bool + Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the quantity defined in the + regularization. If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the + reference model is equal to the starting model for the inversion. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. alpha_s : float, optional - Smallness weight + Scaling constant for the smallness regularization term. alpha_x, alpha_y, alpha_z : float or None, optional - First order smoothness weights for the respective dimensions. - `None` implies setting these weights using the `length_scale` - parameters. + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional - A mapping to apply to the model before regularization. - reference_model : array_like, optional - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict of array_like, optional - User defined weights. It is recommended to interact with weights using - the :py:meth:`~get_weights`, :py:meth:`~set_weights` methods. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the + total gradient or components of the gradient. + norms : (dim+1, ) numpy.ndarray + The respective norms used for the sparse smallness, x-smoothness, (y-smoothness + and z-smoothness) regularization function. Must all be within the interval [0, 2]. + E.g. `np.r_[2, 1, 1, 1]` uses a 2-norm on the smallness term and a 1-norm on all + smoothness terms. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization + function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. Notes ----- - The model objective function :math:`\phi_m (m)` defined by the weighted sum of sparse smallness and smoothness - regularization functions is given by: + The model objective function :math:`\phi_m (m)` defined by a weighted sum of + :class:`SparseSmallness` and :class:`SparseSmoothness` regularization functions + is given by: .. math:: - \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) \Big [ m(r) - m_{ref}(r) \Big ]^{p_s} \, dv + - \sum_{i=x,y,z} \frac{\alpha_i}{2} \int_\Omega \, w(r) \Bigg ( \frac{\partial}{\partial \xi_i} \Big [ m(r) - m_{ref}(r) \Big ] \Bigg )^{p_i} \, dv + \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) + \Big [ m(r) - m_{ref}(r) \Big ]^{p_s} \, dv + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{q_j} - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model that may or may not be - included in the smoothness terms, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_i` is - the unit direction along :math:`i`. Constants :math:`\alpha_s` and :math:`\alpha_i` weight the respective - contributions of the smallness and first-order smoothness regularization functions. By our definition, - :math:`m(r)`, :math:`m_{ref}(r)` and :math:`w(r)` are continuous variables as a function of location :math:`r`. + where :math:`m(r)` is the model, :math:`m_{ref}(r)` is the reference model, and :math:`w(r)` + is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. + Constants :math:`\alpha_s` and :math:`\alpha_j` weight the respective + contributions of the smallness and smoothness regularization functions. + :math:`p_s` is the norm for the smallness term and :math:`q_j` are the norms for the + smoothness terms. + + For implementation within SimPEG, the regularization function and its variables + are discretized onto a `mesh`. For the smallness regularization term: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_s} + + where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}_s}` are amalgamated weighting constants that account + for cell dimensions in the discretization and apply user-defined weighting. + Likewise for the smoothness terms. + + + - For practical implementation within SimPEG, the model objective function and the aforementioned variables - are discretized onto a mesh; set upon instantiation. The discrete approximation to the model objective function is given by: + For practical implementation within SimPEG, the model objective function and the + aforementioned variables are discretized onto a mesh; set upon instantiation. + The discrete approximation to the model objective function is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} \| \mathbf{R_s W_s} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \sum_{i=x,y,z} \frac{\alpha_i}{2} \| \mathbf{R_i W_i G_i} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \Big \| \mathbf{W_s R_s} (\mathbf{m} - \mathbf{m_{ref}} ) \Big \|^2 + + \sum_{i=x,y,z} \frac{\alpha_i}{2} + \Big \| \mathbf{W_i R_i G_i} [\mathbf{m} - \mathbf{m_{ref}} ] \Big \|^2 where @@ -503,8 +709,39 @@ class Sparse(WeightedLeastSquares): - :math:`\mathbf{W}` are weighting matrices, and - :math:`\mathbf{R}` are the IRLS re-weighting matrices - See the documentation for :class:`SparseSmallness` and :class:`SparseSmoothness` - for more details on how weighting matrices are constructed for each term. + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights. + These weights are applied to every term in the sparse regularization. Each set of custom cell weights is + defined by a ``key`` and an (n_cells, ) ``numpy.ndarray`` within a ``dict``. The weights can be set all at + once during instantiation using the `weights` keyword argument: + + >>> reg = Sparse(mesh, weights={'weights_1': w1, 'weights_2': w2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights({'weights_1': w1, 'weights_2': w2}) + + For more information on how custom weights are implemented in the regularization, see the documentation + for :class:`SparseSmallness` and :class:`SparseSmoothness`. + + **IRLS weights and the re-weighting matrix:** + + IRLS weights allow each term in the sparse regularization to be approximated by a weighted 2-norm. + The IRLS weights for each term are model-dependent and must be computed at every IRLS iteration. + For iteration :math:`k`, the IRLS weights :math:`\mathbf{w_r}` are updated internally + using the previous model via: + + .. math:: + \mathbf{w_r}(\mathbf{m}_k) = \lambda (\mathbf{m}) \Big [ \mathbf{m}_{k-1}^2 + \epsilon^2 \Big ]^{p/2 - 1} + + where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). + :math:`\lambda (\mathbf{m})` is an optional scaling constant (``bool`` set with `irls_scaled`). The scaling constant + ensures the sparse norm has roughly the same magnitude as the equivalent 2-norm for the same model. + The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where + + .. math:: + \mathbf{R} = \textrm{diag} \Big ( \mathbf{A_{fx}} \mathbf{w_r}^{1/2} \Big ) """ def __init__( @@ -565,8 +802,12 @@ def __init__( @property def gradient_type(self) -> str: - """ - Choice of gradient measure used in the irls weights + """Choice of gradient measure for IRLS weights. + + Returns + ------- + {"total", "component"} + Choice of gradient measure for IRLS weights. """ return self._gradient_type @@ -584,8 +825,12 @@ def gradient_type(self, value: str): @property def norms(self): - """ - Value of the norm + """Norms for each child regularization. + + Returns + ------- + list of float or numpy.ndarray + Norms for each child regularization. """ return self._norms @@ -612,8 +857,12 @@ def norms(self, values: list | np.ndarray | None): @property def irls_scaled(self) -> bool: - """ - Scale irls weights. + """Scale IRLS weights. + + Returns + ------- + bool + Scale the IRLS weights. """ return self._irls_scaled @@ -626,8 +875,15 @@ def irls_scaled(self, value: bool): @property def irls_threshold(self): - """ + """IRLS stabilization constant. + Constant added to the denominator of the IRLS weights for stability. + See documentation for the :class:`Sparse` class for a comprehensive description. + + Returns + ------- + float + IRLS stabilization constant. """ return self._irls_threshold @@ -641,8 +897,12 @@ def irls_threshold(self, value): self._irls_threshold = value def update_weights(self, model): - """ - Trigger irls update on all children + """Trigger IRLS update for all child regularization functions. + + Parameters + ---------- + model + The model. """ for fct in self.objfcts: fct.update_weights(model) From b6b0929841af20e7375271ef553473ababb4a3f5 Mon Sep 17 00:00:00 2001 From: dccowan Date: Tue, 16 May 2023 15:38:38 -0700 Subject: [PATCH 076/455] edits to sparse and WLS regularization classes --- SimPEG/regularization/base.py | 345 +++++++++++++++++++------------- SimPEG/regularization/sparse.py | 264 +++++++++++++++--------- 2 files changed, 379 insertions(+), 230 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index b27759d7e5..aa9e230464 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -361,8 +361,9 @@ def remove_weights(self, key): def W(self) -> np.ndarray: r"""Weighting matrix. - Returns the weighting matrix for the discrete regularization function. For a comprehensive - description of the weighting matrix, the *Notes* section for the :class:`Smallness` class. + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`Smallness` + regularization class. Returns ------- @@ -519,26 +520,26 @@ class Smallness(BaseRegularization): Notes ----- - The regularization function for smallness is defined as: + We define the regularization function for smallness as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w( \mathscr{r}) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` is a user-defined weighting function. For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. The general form of the discrete regularization + must be discretized onto a `mesh`. The discretized approximation for the regularization function is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account - for cell dimensions in the discretization and apply user-defined weighting. In terms of linear - operators, the discrete regularization function can be expressed as: + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of + linear operators, the discrete regularization function can be expressed as: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} @@ -560,14 +561,14 @@ class Smallness(BaseRegularization): where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. - The weights are implemented using a weighting matrix given by: + The weighting matrix used to apply the weights is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) - ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) @@ -606,13 +607,14 @@ def f_m(self, m) -> np.ndarray: Notes ----- - The discretized form of the smallness regularization function is expressed as: + The discretized approximation for the smallness regularization function is given by + (see documentation for the :class:`Smallness` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{m}` are the discrete model parameters (model), + where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is the weighting matrix. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -625,8 +627,6 @@ def f_m(self, m) -> np.ndarray: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 - For a more comprehensive description of the regularization function, - see the *Notes* section with documentation for the :class:`Smallness` class. """ return self.mapping * self._delta_m(m) @@ -640,7 +640,6 @@ def f_m_deriv(self, m) -> csr_matrix: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} where :math:`\mathbf{I}` is the identity matrix: - For a more detailed description, see the *Notes* section below. Parameters ---------- @@ -654,7 +653,8 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the smallness regularization function is expressed as: + The discretized approximation for the smallness regularization function is given by + (see documentation for the :class:`Smallness` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -678,9 +678,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. For a more comprehensive - description of the regularization function, see the *Notes* section - with documentation for the :class:`Smallness` class. + where :math:`\mathbf{I}` is the identity matrix. """ return self.mapping.deriv(self._delta_m(m)) @@ -718,13 +716,16 @@ class SmoothnessFirstOrder(BaseRegularization): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to - a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh`. + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. Notes ----- - The regularization function for first-order smoothness along the x-direction is given by: + We define the regularization function for first-order smoothness along the x-direction as: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -733,17 +734,17 @@ class SmoothnessFirstOrder(BaseRegularization): where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. The general form of the discrete regularization + must be discretized onto a `mesh`. The discretized approximation for the regularization function is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account - for cell dimensions in the discretization and apply user-defined weighting. In terms of linear - operators, the discrete regularization function can be expressed as: + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of + linear operators, the discrete regularization function can be expressed as: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x m } \, \Big \|^2 @@ -758,9 +759,9 @@ class SmoothnessFirstOrder(BaseRegularization): **Reference model in smoothness:** - Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by - including the reference model the smoothness regularization function. In this case, - the regularization function is given by: + Gradients/interfaces within a reference model :math:`m^{(ref)}` can be preserved by + including the reference model in the smoothness regularization function. In this case, + the regularization function is expressed as: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -779,26 +780,34 @@ class SmoothnessFirstOrder(BaseRegularization): **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the discrete regularization function - is given by: + custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for + smoothness along the x-direction. Each set of weights were either defined directly on the + faces or have been averaged from cell centers. + + The weighting applied within the discrete regularization function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{A_{cx}} - \bigg [ \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} \bigg ] + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. - :math:`\mathbf{A_{cfx}}` projects cell variables to x-faces (where the x-derivative lives). - The weights are implemented using a weighting matrix given by: + and dimensions when the regularization function is discretized onto the mesh. + The weighting matrix is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) - ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: - >>> reg = SmoothnessFirstOrder(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers + >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces + >>> reg = SmoothnessFirstOrder( + >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) or set after instantiation using the `set_weights` method: @@ -889,16 +898,16 @@ def _multiplier_pair(self): def f_m(self, m): r"""Evaluate least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization in the x-direction, + For first-order smoothness regularization in the x-direction, the least-squares regularization kernel is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)}} \big ] + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction - (i.e. x-derivative), :math:`\mathbf{m}` are the descrite model parameters and - :math:`\mathbf{m}^{(ref)}` is the reference model. For a more detailed description - see the notes. + (i.e. x-derivative), :math:`\mathbf{m}` are the descrete model parameters defined on the + mesh and :math:`\mathbf{m}^{(ref)}` is the reference model. Similarly for smoothness + along y and z. Parameters ---------- @@ -912,8 +921,9 @@ def f_m(self, m): Notes ----- - The discretized form of the first order smoothness regularization function along - the x-direction is expressed as: + The discretized approximation of the regularization function for first-order smoothness + along the x-direction is given by (see documentation for the + :class:`SmoothnessFirstOrder` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -922,8 +932,9 @@ def f_m(self, m): where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + the weighting matrix. Similar for smoothness along y and z. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -931,10 +942,7 @@ def f_m(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x \, f_m} \Big \|^2 - - For a more comprehensive description of the regularization function, see the - *Notes* section of the documentation for the :class:`SmoothnessFirstOrder` class. + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -948,14 +956,14 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessFirstOrder`` regularization, the least-squares regularization kernel - is given by: + For first-order smoothness regularization in the x-direction, the derivative of the + least-squares regularization kernel with respect to the model is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} - And thus, the derivative with respect to the model is the x-derivative - operator :math:`\mathbf{G_x}`. For a more detailed, description, see the notes. + where :math:`\mathbf{G_x}` is the partial cell gradient operator along x + (i.e. the x-derivative). Parameters ---------- @@ -969,8 +977,9 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the first order smoothness regularization function along - the x-direction is expressed as: + The discretized approximation of the regularization function for first-order smoothness + along the x-direction is given by (see documentation for the + :class:`SmoothnessFirstOrder` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -979,8 +988,9 @@ def f_m_deriv(self, m) -> csr_matrix: where :math:`\mathbf{m}` are the discrete model parameters (model), :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + the weighting matrix. Similar for smoothness along y and z. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -988,16 +998,12 @@ def f_m_deriv(self, m) -> csr_matrix: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} - \Big \| \mathbf{W G_x} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 The derivate with respect to the model is therefore: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} - - For a more comprehensive description of the regularization function, see the *Notes* - section of the documentation for the :class:`SmoothnessFirstOrder` class. """ return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) @@ -1005,9 +1011,9 @@ def f_m_deriv(self, m) -> csr_matrix: def W(self): r"""Weighting matrix. - Returns the weighting matrix for the discrete regularization function. For a comprehensive - description of the weighting matrix, the *Notes* section for the - :class:`SmoothnessFirstOrder` class. + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`SmoothnessFirstOrder` + regularization class. Returns ------- @@ -1085,7 +1091,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): Notes ----- - The regularization function for second-order smoothness along the x-direction is given by: + We define the regularization function for second-order smoothness along the x-direction as: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -1094,17 +1100,17 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. The general form of the discrete regularization + must be discretized onto a `mesh`. The discretized approximation for the regularization function is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account - for cell dimensions in the discretization and apply user-defined weighting. In terms of linear - operators, the discrete regularization function can be expressed as: + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of + linear operators, the discrete regularization function can be expressed as: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 @@ -1145,16 +1151,16 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. - The weights are implemented using a weighting matrix given by: + The weighting matrix used to apply the weights is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) - ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: - >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + >>> reg = SmoothnessSecondOrder(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) or set after instantiation using the `set_weights` method: @@ -1165,14 +1171,13 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): def f_m(self, m): r"""Evaluate least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization in the x-direction, + For second-order smoothness regularization in the x-direction, the least-squares regularization kernel is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator. - For a more detailed description, see the notes. Parameters ---------- @@ -1186,18 +1191,20 @@ def f_m(self, m): Notes ----- - The discretized form of the second order smoothness regularization function along - the x-direction is expressed as: + The discretized approximation of the regularization function for second-order smoothness + along the x-direction is given by (see documentation for the + :class:`SmoothnessSecondOrder` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, where :math:`\mathbf{L_x}` is the - discrete second order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second + order derivative operator with respect to x, and :math:`\mathbf{W}` is + the weighting matrix. Similar for smoothness along y and z. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1205,11 +1212,7 @@ def f_m(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} - \Big \| \mathbf{W L_x} \, \mathbf{f_m} \Big \|^2 - - For a more comprehensive description of the regularization function, see the *Notes* - section with documentation for the :class:`SmoothnessSecondOrder` class. + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 """ dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) @@ -1226,20 +1229,13 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For ``SmoothnessSecondOrder`` regularization, the least-squares regularization - kernel is given by: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, - :math:`\mathbf{m}` are the dicrete model parameters, :math:`\mathbf{m}^{(ref)}` - is a reference model. And thus, the derivative with respect to the model is + For second-order smoothness regularization, the derivative of the least-squares regularization + kernel with respect to the model is given by: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - For a more detailed, description, see the notes. + where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. Parameters ---------- @@ -1253,18 +1249,20 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized form of the second order smoothness regularization function along - the x-direction is expressed as: + The discretized approximation of the regularization function for second-order smoothness + along the x-direction is given by (see documentation for the + :class:`SmoothnessSecondOrder` class for further reading): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, where :math:`\mathbf{L_x}` is the - discrete second order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second + order derivative operator with respect to x, and :math:`\mathbf{W}` is + the weighting matrix. Similar for smoothness along y and z. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1272,16 +1270,12 @@ def f_m_deriv(self, m) -> csr_matrix: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} - \Big \| \mathbf{W L_x} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 - The derivate with respect to the model is: + The derivate of the regularization kernel with respect to the model is: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - - For a more comprehensive description of the regularization function, see the - *Notes* section with documentation for the :class:`SmoothnessSecondOrder` class. """ return ( self.cell_gradient.T @@ -1291,7 +1285,17 @@ def f_m_deriv(self, m) -> csr_matrix: @property def W(self): - # Docstring inherited by BaseRegularization class. + r"""Weighting matrix. + + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`SmoothnessSecondOrder` + regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the regularization. + """ if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) self._W = utils.sdiag(weights**0.5) @@ -1374,40 +1378,103 @@ class WeightedLeastSquares(ComboObjectiveFunction): \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^2 \, dv \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) \bigg [ \frac{\partial^2 m}{\partial \xi_j^2} \bigg ]^2 \, dv - \;\;\;\;\; ( \leftarrow \, optional ) + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. - Constants :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` weight the respective - contributions of the smallness and smoothness regularization functions. + Constants :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` for :math:`j=x,y,z` + weight the respective contributions of the smallness and smoothness regularization functions. For implementation within SimPEG, regularization functions and their variables - are discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose - discrete representation is given by :math:`\mathbf{x}`: + must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose + discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate + as follows: .. math:: - \int_\Omega w(r) \big [ x(r) \big ]^2 \, dv = \sum_i \tilde{w}_i \, | x_i |^2 + \int_\Omega w(r) \big [ x(r) \big ]^2 \, dv \approx \sum_i \tilde{w}_i \, | x_i |^2 - :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions in - the discretization and apply user-defined weighting. - - For ``Sparse`` regularization, the discretized model objective function is given by: + where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions + in the discretization and apply user-defined weighting. Using the above approximation, + the discretized model objective function for ``WeightedLeastSquares`` regularization + can be expressed as: .. math:: \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 - \;\;\;\;\; ( \leftarrow \, optional ) + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where - - :math:`\mathbf{G_j}` are partial cell gradients operators along x, y and z, - - :math:`\mathbf{L_j}` are second-order derivative operators with respect to x, y and z, - - :math:`\mathbf{W}` are weighting matrices. + - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), + - :math:`\mathbf{m}^{(ref)}` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices. + + **Alphas and length scales:** + + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` parameters are set to 0 by default. + + The model objective function has been formulated such that each term is + roughly the same size when the :math:`\alpha` parameters are equal; e.g. when + :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying a default + weighting to each term which negates the effects of cell size/dimensions. + + Smoothness parameters can also be set using length scales. For example, by setting the + `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: + + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. - **Alpha weighting parameters:** + **Custom weights and weighting matrices:** + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell + weights that are applied to all regularization terms in the model objective function. + The general form for the weights applied to smallness and second-order smoothness terms + is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + + and weights applied to first-order smoothness terms are given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{P \, w_k} + + :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell volumes + and dimensions when regularization functions are discretized onto the mesh. These default + weights vary depending on the regularization term; e.g. smallness, first-order smoothness in + the y-direction. For first-order smoothness terms, a projection matrix :math:`\mathbf{P}` is + required to average weights at cell centers to the appropriate faces; i.e. where discrete + first-order derivatives live. + + Weights for each regularization term are used to construct their respective weighting matrices + as follows: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = WeightedLeastSquares(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) **Reference model in smoothness:** @@ -1422,7 +1489,7 @@ class WeightedLeastSquares(ComboObjectiveFunction): \bigg [ \frac{\partial}{\partial \xi_j} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) \bigg [ \frac{\partial^2}{\partial \xi_j^2} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 - \;\;\;\;\; ( \leftarrow \, optional ) + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) When discretized onto a mesh, the regularization function is approximated by: @@ -1433,7 +1500,7 @@ class WeightedLeastSquares(ComboObjectiveFunction): \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - \;\;\;\;\; ( \leftarrow \, optional ) + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index b0efa538ca..e7641751b4 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -223,7 +223,7 @@ class SparseSmallness(BaseSparse, Smallness): Notes ----- - The regularization function for sparse smallness (compactness) is given by: + We define the regularization function for sparse smallness (compactness) as: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -233,7 +233,7 @@ class SparseSmallness(BaseSparse, Smallness): is a user-defined weighting function. For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. The general form of the discrete regularization + must be discretized onto a `mesh`. The discretized approximation for the regularization function is given by: .. math:: @@ -252,7 +252,7 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \gamma \big (\mathbf{m}^{(k)} \big ) = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^p - \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big [ m_i^{(k)} - m_i^{(ref)} \Big ]^p + \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -265,8 +265,8 @@ class SparseSmallness(BaseSparse, Smallness): iteration :math:`k` can be expressed as follows: .. math:: - \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \bigg \| \, - \mathbf{W \, R} \, \Big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \Big ] \bigg \|^2 + \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W \, R} \, \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where @@ -278,22 +278,22 @@ class SparseSmallness(BaseSparse, Smallness): **IRLS weights and the re-weighting matrix:** The IRLS weights are model-dependent and must be computed at every IRLS iteration. - For iteration :math:`k`, the IRLS weights :math:`\mathbf{w_r}` are updated internally + For iteration :math:`k`, the IRLS weights :math:`\mathbf{r_s}` are updated internally using the previous model via: .. math:: - \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) + \mathbf{r_s} \big ( \mathbf{m}^{(k)} \big ) = \lambda_s \big ( \mathbf{m}^{(k-1)} \big ) \bigg [ \Big ( \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big )^2 + \epsilon^2 \bigg ]^{p/2 - 1} where :math:`\epsilon` is a small constant added for stability of the algorithm - (set using `irls_threshold`). :math:`\lambda (\mathbf{m})` is an optional scaling + (set using `irls_threshold`). :math:`\lambda_s (\mathbf{m})` is an optional scaling constant (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse norm has roughly the same magnitude as the equivalent 2-norm for the same model. The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where .. math:: - \mathbf{R} = \textrm{diag} \Big ( \mathbf{w_r}^{1/2} \Big ) + \mathbf{R} = \textrm{diag} \Big ( \mathbf{r_s}^{\! 1/2} \Big ) **Custom weights and the weighting matrix:** @@ -306,14 +306,14 @@ class SparseSmallness(BaseSparse, Smallness): where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. - The weights are implemented using a weighting matrix given by: + The weighting matrix used to apply the weights is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) - ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) @@ -377,9 +377,12 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to - a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh`. + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. irls_scaled : bool If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. If ``False``, do not scale. @@ -391,8 +394,8 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): Notes ----- - The regularization function for sparse smoothness (blockiness) along the x-direction - is given by: + We define the regularization function for sparse smoothness (blockiness) along the x-direction + as: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -403,7 +406,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): recovery structures with sharper boundaries. For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. The general form of the discrete regularization + must be discretized onto a `mesh`. The discrete approximation for the regularization function is given by: .. math:: @@ -424,7 +427,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): = \frac{1}{2} \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^p \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} - \Bigg ( \, \frac{\partial \tilde{m}_i^{(k)}}{\partial x} \Bigg )^2 + \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -452,73 +455,83 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): **IRLS weights and the re-weighting matrix:** - At every IRLS iteration, the IRLS weights :math:`\mathbf{w_r}` are recomputed internally - using the previous model via: + For smoothness along the x-direction, the IRLS weights :math:`\mathbf{r_x}` at every iteration + are recomputed internally using the previous iteration via: .. math:: - \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) - \Bigg [ \bigg ( \mathbf{G_x \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \bigg )^2 - + \epsilon^2 \bigg ]^{p/2 - 1} + \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda_x \big ( \mathbf{m}^{(k-1)} \big ) + \bigg [ \Big ( \mathbf{G_x} \big [ \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \big ] \Big )^2 + + \epsilon^2 \bigg ]^{q_x/2 - 1} where :math:`\epsilon` is a small constant added for stability of the algorithm - (set using `irls_threshold`) and :math:`\lambda (\mathbf{m})` is an optional scaling constant + (set using `irls_threshold`) and :math:`\lambda_x (\mathbf{m})` is an optional scaling constant (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse norm has roughly the same magnitude as the equivalent 2-norm for the same model. The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where .. math:: - \mathbf{R} = \textrm{diag} \Big ( \mathbf{w_r}^{1/2} \Big ) + \mathbf{R} = \textrm{diag} \Big ( \mathbf{r_x}^{\! 1/2} \Big ) + + Likewise for smoothness along y or z. **Reference model in smoothness:** Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by - including the reference model the smoothness regularization function. In this case, - the regularization function is given by: + including the reference model the smoothness regularization function. For smoothness along + the x-direction, the regularization function is given by: .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg [ \frac{\partial}{\partial x} \Big ( m - m^{(ref)} \Big ) \bigg ]^p \, dv - When discretized onto a mesh, the regularization function is approximated by: + When discretized onto a mesh, the regularization function becomes: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \bigg \| \mathbf{W R G_x} - \Big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \Big ] \bigg \|^2 + \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} + \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where the IRLS weights are given by: + And the IRLS weights are at iteration :math:`k` are given by: .. math:: - \mathbf{w_r} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) + \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) \Bigg [ \bigg ( \mathbf{G_x} \Big [ \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big ] \bigg )^2 + \epsilon^2 \bigg ]^{p/2 - 1} - This functionality is used by setting :math:`\mathbf{m^{(ref)}}` with the + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the discrete regularization function - is given by: + custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for + smoothness along the x-direction. Each set of weights were either defined directly on the + faces or have been averaged from cell centers. + + The weighting applied within the discrete regularization function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{A_{cx}} - \bigg [ \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} \bigg ] + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. - :math:`\mathbf{A_{cfx}}` projects cell variables to x-faces (where the x-derivative lives). - The weights are implemented using a weighting matrix given by: + and dimensions when the regularization function is discretized onto the mesh. + The weighting matrix is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - Each set of custom cell weights is defined by a ``key`` and an (n_cells, ) - ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: - >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers + >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces + >>> reg = SparseSmoothness( + >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) or set after instantiation using the `set_weights` method: @@ -665,83 +678,150 @@ class Sparse(WeightedLeastSquares): .. math:: \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) - \Big [ m(r) - m_{ref}(r) \Big ]^{p_s} \, dv + \Big [ m(r) - m^{(ref)}(r) \Big ]^{p_s} \, dv + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) - \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{q_j} + \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{q_j} \, dv - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is the reference model, and :math:`w(r)` + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. - Constants :math:`\alpha_s` and :math:`\alpha_j` weight the respective + Constants :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` weight the respective contributions of the smallness and smoothness regularization functions. - :math:`p_s` is the norm for the smallness term and :math:`q_j` are the norms for the - smoothness terms. + :math:`p_s` is the norm for the smallness term and :math:`q_j` for :math:`j=x,y,z` + are the norms for the smoothness terms. - For implementation within SimPEG, the regularization function and its variables - are discretized onto a `mesh`. For the smallness regularization term: + For implementation within SimPEG, regularization functions and their variables + must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose + discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate + as follows: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i - \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_s} + \int_\Omega w(r) \big [ x(r) \big ]^p \, dv \approx \sum_i \tilde{w}_i \, | x_i |^p - where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}_s}` are amalgamated weighting constants that account - for cell dimensions in the discretization and apply user-defined weighting. - Likewise for the smoothness terms. + where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions + in the discretization and apply user-defined weighting. + It is impractical to work with sparse norms directly, as their derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate sparse norms by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + .. math:: + \sum_i \tilde{w}_i \, \Big | x_i^{(k)} \Big |^p + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | x_i^{(k)} \Big |^2 - - For practical implementation within SimPEG, the model objective function and the - aforementioned variables are discretized onto a mesh; set upon instantiation. - The discrete approximation to the model objective function is given by: + where IRLS weight :math:`r_i` for iteration :math:`k` is obtained from the previous iteration via: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( x_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p/2 - 1} + + and :math:`\epsilon` is a small constant added for stability. For the set of model parameters + :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS + iteration :math:`k` can be expressed as follows: .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} - \Big \| \mathbf{W_s R_s} (\mathbf{m} - \mathbf{m_{ref}} ) \Big \|^2 - + \sum_{i=x,y,z} \frac{\alpha_i}{2} - \Big \| \mathbf{W_i R_i G_i} [\mathbf{m} - \mathbf{m_{ref}} ] \Big \|^2 + \Big \| \mathbf{W_s R_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j R_j G_j m} ] \Big \|^2 where - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), - - :math:`\mathbf{m_{ref}}` is a reference model which may or may not be inclulded in the smoothess terms, - - :math:`\mathbf{G_i}` are partial cell gradients operators along x, y and z, - - :math:`\mathbf{W}` are weighting matrices, and - - :math:`\mathbf{R}` are the IRLS re-weighting matrices + - :math:`\mathbf{m}^{(ref)}` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices, and + - :math:`\mathbf{R_s, \, R_x, \, R_y, \; R_z}` are IRLS re-weighting matrices - **Custom weights and the weighting matrix:** + **Alphas and length scales:** - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights. - These weights are applied to every term in the sparse regularization. Each set of custom cell weights is - defined by a ``key`` and an (n_cells, ) ``numpy.ndarray`` within a ``dict``. The weights can be set all at - once during instantiation using the `weights` keyword argument: + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``Sparse`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` properties are set to 0 by default. - >>> reg = Sparse(mesh, weights={'weights_1': w1, 'weights_2': w2}) + The model objective function has been formulated such that each term is + roughly the same size when the :math:`\alpha` parameters are equal; e.g. when + :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying + a default weighting to each term which negates the effects of cell size/dimensions. - or set after instantiation using the `set_weights` method: + Smoothness parameters can also be set using length scales. For example, by setting the + `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: - >>> reg.set_weights({'weights_1': w1, 'weights_2': w2}) + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 - For more information on how custom weights are implemented in the regularization, see the documentation - for :class:`SparseSmallness` and :class:`SparseSmoothness`. + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. + + **Custom weights and weighting matrices:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell + weights that are applied to all regularization terms in the model objective function. + The general form for the weights applied to the smallness term is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + + And the general form for the weights applied to first-order smoothness terms is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{P \, w_k} + + :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell volumes + and dimensions when regularization functions are discretized onto the mesh. These default + weights vary depending on the regularization term; e.g. smallness, first-order smoothness in + the y-direction. For first-order smoothness terms, a projection matrix :math:`\mathbf{P}` is + required to average weights at cell centers to the appropriate faces; i.e. where discrete + first-order derivatives live. + + Weights for each regularization term are used to construct their respective weighting matrices + as follows: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = Sparse(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) **IRLS weights and the re-weighting matrix:** IRLS weights allow each term in the sparse regularization to be approximated by a weighted 2-norm. The IRLS weights for each term are model-dependent and must be computed at every IRLS iteration. - For iteration :math:`k`, the IRLS weights :math:`\mathbf{w_r}` are updated internally - using the previous model via: + For iteration :math:`k`, the IRLS weights :math:`\mathbf{r_s}` for the smallness term + are updated internally using the previous model via: .. math:: - \mathbf{w_r}(\mathbf{m}_k) = \lambda (\mathbf{m}) \Big [ \mathbf{m}_{k-1}^2 + \epsilon^2 \Big ]^{p/2 - 1} + \mathbf{r_s} \big ( \mathbf{m}^{(k)} \big ) = \lambda_s \big ( \mathbf{m}^{(k-1)} \big ) + \bigg [ \Big ( \big ( \mathbf{m}^{(k-1)} \big ) \Big )^2 + \epsilon^2 \bigg ]^{p_s/2 - 1} - where :math:`\epsilon` is a small constant added for stability of the algorithm (set using `irls_threshold`). - :math:`\lambda (\mathbf{m})` is an optional scaling constant (``bool`` set with `irls_scaled`). The scaling constant - ensures the sparse norm has roughly the same magnitude as the equivalent 2-norm for the same model. - The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where + For first-order smoothness along the x-direction (likewise for smoothness in y and z), + the IRLS weights :math:`\mathbf{r_x}` are updated via: .. math:: - \mathbf{R} = \textrm{diag} \Big ( \mathbf{A_{fx}} \mathbf{w_r}^{1/2} \Big ) + \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda_x \big ( \mathbf{m}^{(k-1)} \big ) + \bigg [ \Big ( \big ( \mathbf{G_x m}^{(k-1)} \big ) \Big )^2 + \epsilon^2 \bigg ]^{q_x/2 - 1} + + where :math:`\epsilon` is a small constant added for stability of the algorithm + (set using `irls_threshold`). :math:`\lambda` are optional scaling constants + (``bool`` set with `irls_scaled`) applied to each set of IRLS weights. + The scaling constant ensures the sparse norm has roughly the same magnitude as the + equivalent 2-norm for the same model. The IRLS weights are then used to construct their + respective re-weighting matrices :math:`\mathbf{R_\ast}` as follows: + + .. math:: + \mathbf{R_\ast} = \textrm{diag} \Big ( \mathbf{r_\ast}^{\! 1/2} \Big ) + + where :math:`\ast` represents :math:`s, x, y` or :math:`z`. """ def __init__( @@ -825,12 +905,14 @@ def gradient_type(self, value: str): @property def norms(self): - """Norms for each child regularization. + """Norms for the child regularization classes. + + Supply the norms for the smallness and all smoothness terms in the ``Sparse`` regularization. Returns ------- list of float or numpy.ndarray - Norms for each child regularization. + Norms for the child regularization classes. """ return self._norms From 66f951b2751c8f2303ba0f8b1a4902c125a2e33d Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 17 May 2023 10:10:25 -0700 Subject: [PATCH 077/455] Use joe's reshape comment --- SimPEG/regularization/vector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index d56e6052a1..c2a0884365 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -134,8 +134,8 @@ def deriv(self, m) -> np.ndarray: d_m = self._delta_m(m) return self.f_m_deriv(m).T * ( - sp.block_diag([self.W.T * self.W] * self.n_comp) * self.f_m_deriv(m) * d_m - ) + self.W.T @ self.W @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order='F') + ).flatten(order='F') def deriv2(self, m, v=None) -> csr_matrix: """ """ @@ -147,8 +147,8 @@ def deriv2(self, m, v=None) -> csr_matrix: ) return f_m_deriv.T * ( - sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv * v - ) + self.W.T @ self.W @ (f_m_deriv * v).reshape((-1, self.n_comp), order='F') + ).flatten(order='F') class AmplitudeSmallness(SparseSmallness, BaseAmplitude): From 2c49778d28744375d362fb59e0d5c76bf1998951 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 17 May 2023 10:10:54 -0700 Subject: [PATCH 078/455] run black --- SimPEG/regularization/vector.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index c2a0884365..32e1cf9c47 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -134,8 +134,10 @@ def deriv(self, m) -> np.ndarray: d_m = self._delta_m(m) return self.f_m_deriv(m).T * ( - self.W.T @ self.W @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order='F') - ).flatten(order='F') + self.W.T + @ self.W + @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") def deriv2(self, m, v=None) -> csr_matrix: """ """ @@ -147,8 +149,8 @@ def deriv2(self, m, v=None) -> csr_matrix: ) return f_m_deriv.T * ( - self.W.T @ self.W @ (f_m_deriv * v).reshape((-1, self.n_comp), order='F') - ).flatten(order='F') + self.W.T @ self.W @ (f_m_deriv * v).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") class AmplitudeSmallness(SparseSmallness, BaseAmplitude): From 43d3494a8d87667f6abf14149449d7693d7ee570 Mon Sep 17 00:00:00 2001 From: dccowan Date: Thu, 18 May 2023 20:45:20 -0700 Subject: [PATCH 079/455] final draft of WLS and sparse regularization classes --- SimPEG/regularization/base.py | 146 ++++---- SimPEG/regularization/sparse.py | 594 +++++++++++++++++++------------- 2 files changed, 422 insertions(+), 318 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index aa9e230464..d2186be31d 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -489,8 +489,8 @@ class Smallness(BaseRegularization): ``Smallness`` regularization is used to ensure that differences between the model values in the recovered model and the reference model are small; - i.e. it preserves structures in the reference model. If the reference model is - ``None``, the starting model will be set as the reference model in the + i.e. it preserves structures in the reference model. If a reference model is not + supplied, the starting model will be set as the reference model in the regularization by default. Optionally, custom cell weights can be included to control the degree of smallness being enforced throughout different regions the model. @@ -508,15 +508,15 @@ class Smallness(BaseRegularization): The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting - is set as the reference model by default. + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model in the inversion. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh`. + :py:class:`regularization.RegularizationMesh` . Notes ----- @@ -561,7 +561,7 @@ class Smallness(BaseRegularization): where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. - The weighting matrix used to apply the weights is given by: + The weighting matrix used to apply the weights for smallness regularization is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) @@ -570,12 +570,16 @@ class Smallness(BaseRegularization): ``numpy.ndarray``. The weights can be set all at once during instantiation with the `weights` keyword argument as follows: - >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + >>> reg = Smallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) or set after instantiation using the `set_weights` method: >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights['volume'] + """ _multiplier_pair = "alpha_s" @@ -587,7 +591,7 @@ def __init__(self, mesh, **kwargs): def f_m(self, m) -> np.ndarray: r"""Evaluate least-squares regularization kernel. - For ``Smallness`` regularization, the least-squares regularization kernel is given by: + For smallness regularization, the least-squares regularization kernel is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -708,8 +712,9 @@ class SmoothnessFirstOrder(BaseRegularization): The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting - is set as the reference model by default. + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model by default. To include the reference model in the + regularization function, the `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -759,21 +764,15 @@ class SmoothnessFirstOrder(BaseRegularization): **Reference model in smoothness:** - Gradients/interfaces within a reference model :math:`m^{(ref)}` can be preserved by - including the reference model in the smoothness regularization function. In this case, - the regularization function is expressed as: - - .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \bigg [ \frac{\partial}{\partial x} \big ( m - m^{(ref)} \big ) \bigg ]^2 \, dv - - When discretized onto a mesh, the regularization function is approximated by: + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization function. + In this case, the discrete regularization function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. @@ -813,6 +812,10 @@ class SmoothnessFirstOrder(BaseRegularization): >>> reg.set_weights(weights_1=array_1, weights_2=array_2) + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights['volume'] + """ def __init__( @@ -906,7 +909,7 @@ def f_m(self, m): where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), :math:`\mathbf{m}` are the descrete model parameters defined on the - mesh and :math:`\mathbf{m}^{(ref)}` is the reference model. Similarly for smoothness + mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). Similarly for smoothness along y and z. Parameters @@ -1077,8 +1080,9 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting - is set as the reference model by default. + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model by default. To include the reference model in the + regularization function, the `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -1122,21 +1126,15 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): **Reference model in smoothness:** - Second-order derivatives within a reference model :math:`m^{(ref)}` can be preserved by - including the reference model the smoothness regularization function. In this case, - the regularization function is given by: - - .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \bigg [ \frac{\partial^2}{\partial x^2} \Big ( m - m^{(ref)} \Big ) \bigg ]^2 \, dv - - When discretized onto a mesh, the regularization function is approximated by: + Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization function. + In this case, the discrete regularization function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. @@ -1177,7 +1175,9 @@ def f_m(self, m): .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator. + where where :math:`\mathbf{m}` are the discrete model parameters (model), + :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` + is the discrete second order x-derivative operator. Parameters ---------- @@ -1200,8 +1200,8 @@ def f_m(self, m): \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second - order derivative operator with respect to x, and :math:`\mathbf{W}` is + :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` is the + second order derivative operator with respect to x, and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -1413,6 +1413,24 @@ class WeightedLeastSquares(ComboObjectiveFunction): - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices. + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization functions. + In this case, the discrete regularization function becomes: + + .. math:: + \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} + \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. + **Alphas and length scales:** The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness @@ -1475,36 +1493,6 @@ class WeightedLeastSquares(ComboObjectiveFunction): or set after instantiation using the `set_weights` method: >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - **Reference model in smoothness:** - - Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by - including the reference model the smoothness regularization functions. In this case, - the regularization function is given by: - - .. math:: - \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) - \Big [ m(r) - m_{ref}(r) \Big ]^2 \, dv \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) - \bigg [ \frac{\partial}{\partial \xi_j} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) - \bigg [ \frac{\partial^2}{\partial \xi_j^2} \Big ( m(r) - m_{ref}(r) \Big ) \bigg ]^2 - \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - - When discretized onto a mesh, the regularization function is approximated by: - - .. math:: - \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} - \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - - This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. """ _model = None @@ -2021,7 +2009,8 @@ def reference_model(self) -> np.ndarray: Returns ------- array_like - Reference model values used to constrain the inversion. If ``None``, the reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, + the reference model is equal to the starting model for the inversion. """ return self._reference_model @@ -2093,7 +2082,8 @@ def units(self, units: str | None): def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. - Mesh on which the regularization is discretized. This is not the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. Returns ------- @@ -2109,7 +2099,8 @@ def mapping(self) -> maps.IdentityMap: Returns ------- SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined on the :py:class:`~SimPEG.regularization.RegularizationMesh`. + The mapping from the model parameters to the quantity defined on the + :py:class:`~SimPEG.regularization.RegularizationMesh`. """ return self._mapping @@ -2150,9 +2141,11 @@ class BaseSimilarityMeasure(BaseRegularization): Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh - Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. + Mesh on which the regularization is discretized. This is not necessarily the same as + the mesh on which the simulation is defined. mapping : SimPEG.maps.WireMap - Wire map connecting physical properties defined on the regularization mesh to the entire model. + Wire map connecting physical properties defined on the regularization mesh + to the entire model. """ def __init__(self, mesh, wire_map, **kwargs): @@ -2166,7 +2159,8 @@ def wire_map(self): Returns ------- SimPEG.maps.WireMap - Wire map connecting physical properties defined on the regularization mesh to the entire model. + Wire map connecting physical properties defined on the regularization + mesh to the entire model. """ return self._wire_map @@ -2197,8 +2191,8 @@ def nP(self): def deriv(self, model): """First derivative of the coupling term with respect to individual models. - Where :math:`k` is the number of models we are inverting for and :math:`M` is the number of cells in each model, - this method returns a vector of length :math:`kM`. + Where :math:`k` is the number of models we are inverting for and :math:`M` is the number + of cells in each model, this method returns a vector of length :math:`kM`. Parameters ---------- @@ -2230,8 +2224,8 @@ def deriv2(self, model, v=None): Returns ------- numpy.ndarray or scipy.sparse.csr_matrix - Where :math:`k` is the number of models we are inverting for and :math:`M` is the number of cells in each model, - this method returns: + Where :math:`k` is the number of models we are inverting for and :math:`M` is + the number of cells in each model, this method returns: - an array of dimensions (k*M, ) when `v` is not ``None``. - a sparse matrix of dimensions (k*M, k*M) when `v` is ``None``. diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index e7641751b4..c53863adf6 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -40,8 +40,8 @@ class BaseSparse(BaseRegularization): The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the - reference model is equal to the starting model for the inversion. + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -68,8 +68,11 @@ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwar def irls_scaled(self) -> bool: """Scale IRLS weights. - When ``True``, the iteratively re-weighted least-squares (IRLS) weights are scaled at - each update to generally preserve the magnitude of the regularization term. + When ``True``, scaling is applied when computing IRLS weights. + The scaling acts to preserve the balance between the data misfit and the model + objective function, and improves convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + For a comprehensive description, see the documentation for :py:meth:`get_lp_weights` . Returns ------- @@ -84,20 +87,12 @@ def irls_scaled(self, value: bool): @property def irls_threshold(self): - r"""Constant added to the denominator of the IRLS weights for stability. - - At each IRLS iteration, the IRLS weights are updated. - The weight at iteration :math:`k` corresponding the cell :math:`i` is given by: - - .. math:: - r_i (\mathbf{m}^{(k)}) = \Big ( ( m_i^{(k-1)})^2 + \epsilon^2 \Big )^{p/2 - 1} - - where the `irls_threshold` :math:`\epsilon` is a constant that stabilizes the expression. + r"""Stability constant for computing IRLS weights. Returns ------- float - Constant added to the denominator of the IRLS weights for stability. + Stability constant for computing IRLS weights. """ return self._irls_threshold @@ -111,18 +106,6 @@ def irls_threshold(self, value): def norm(self): r"""Norm for the sparse regularization. - Sparse-norm regularization functions within SimPEG take the form: - - .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big ( \mathbb{F} \big [ m(r) - m_{ref}(r) \big ] \Bigg )^p \, dv - - where :math:`m(r)` is the model, :math:`m_{ref}(r)` is a reference model, and :math:`w(r)` - is a user-defined weighting function. :math:`\mathbb{F}` is a placeholder for a function that - acts on the difference between :math:`m` and :math:`m_{ref}`; e.g. a differential operator. - - The parameter :math:`p \in [0,2]` defines the norm, where a smaller norm is used to a model - with more sparse structures. - Returns ------- None, float, (n_cells, ) numpy.ndarray @@ -152,16 +135,60 @@ def norm(self, value: float | np.ndarray | None): self._norm = value def get_lp_weights(self, f_m): - """Compute and return standard IRLS weights. + r"""Compute and return iteratively re-weighted least-squares (IRLS) weights. + + For a least-squares regularization kernel function :math:`\mathbf{f_m}(\mathbf{m})` + evaluated at model :math:`\mathbf{m}`, compute and return the IRLS weights. + See :py:meth:`Smallness.f_m` and :py:meth:`SmoothnessFirstOrder.f_m` for examples of + least-squares regularization kernels. - The IRLS weights are applied to ensure the discrete approximation of the sparse - regularization function is evaluated using the specified norm. Since the weighting - is model dependent, it must be recomputed at every IRLS iteration. + For :class:`SparseSmallness`, *f_m* is a (n_cells, ) ``numpy.ndarray``. + For :class:`SparseSmoothness`, *f_m* is a ``numpy.ndarray`` whose length corresponds to + the number of faces along a particular orientation; e.g. for smoothness along x, the length + is (n_faces_x, ). Parameters ---------- - f_m : fcn - The least-squares regularization kernel. + f_m : numpy.ndarray + The least-squares regularization kernel evaluated at the current model. + + Notes + ----- + For a least-squares regularization kernel :math:`\mathbf{f_m}` evaluated at model + :math:`\mathbf{m}`, the IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the `norm` at each cell. + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the model + objective function, and improves convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To apply elementwise scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + And define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\; for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\;\;\;\;\, for \; p_i < 1 + \end{cases} + + The elementwise scaling vector :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] + \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2} \bigg ]^{1 - \mathbf{p}/2} """ lp_scale = np.ones_like(f_m) if self.irls_scaled: @@ -197,8 +224,10 @@ class SparseSmallness(BaseSparse, Smallness): mesh : .regularization.RegularizationMesh Mesh on which the regularization is discretized. Not the mesh used to define the simulation. - norm : float - The norm used in the regularization function. Must be within theinterval [0, 2]. + norm : float, (n_cells, ) array_like + The norm defining sparseness in the regularization function. Use a ``float`` to define + the same norm for all mesh cells, or define an independent norm for each cell. All norm + values must be within the interval [0, 2]. active_cells : None, numpy.ndarray of bool Array of bool defining the set of :py:class:`.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. @@ -206,8 +235,9 @@ class SparseSmallness(BaseSparse, Smallness): The mapping from the model parameters to the quantity defined in the regularization. If ``None``, the mapping is the :class:`.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting - is set as the reference model by default. + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model by default. To include the reference model in the + regularization function, the `reference_model_in_smooth` property must be set to ``True``. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -227,10 +257,13 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \Big | m(r) - m^{(ref)}(r) \Big |^p \, dv + \Big | m(r) - m^{(ref)}(r) \Big |^{p(r)} \, dv - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` - is a user-defined weighting function. + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. More compact structures are recovered in regions + where :math:`p` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization @@ -238,11 +271,12 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i - \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^p + \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_i} - where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that accounts - for cell dimensions in the discretization and applies user-defined weighting. + where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). It is impractical to work with the general form directly, as its derivatives with respect to the model are non-linear and discontinuous. Instead, the iteratively re-weighted @@ -251,65 +285,48 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \gamma \big (\mathbf{m}^{(k)} \big ) - = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^p + = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: .. math:: r_i^{(k)} = \bigg [ \Big ( m_i^{(k-1)} - m_i^{(ref)} \Big )^2 + - \epsilon^2 \; \bigg ]^{p/2 - 1} + \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} - and :math:`\epsilon` is a small constant added for stability. For the set of model parameters - :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS - iteration :math:`k` can be expressed as follows: + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex + least-squares problem for IRLS iteration :math:`k` can be expressed as follows: .. math:: \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, - \mathbf{W \, R} \, \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \mathbf{W}^{\! (k)} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, - :math:`\mathbf{m}^{(ref)}` is a reference model (optional, set with `reference_model`), - - :math:`\mathbf{R}` is the IRLS re-weighting matrix, and - - :math:`\mathbf{W}` is the weighting matrix. + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. - **IRLS weights and the re-weighting matrix:** - - The IRLS weights are model-dependent and must be computed at every IRLS iteration. - For iteration :math:`k`, the IRLS weights :math:`\mathbf{r_s}` are updated internally - using the previous model via: - - .. math:: - \mathbf{r_s} \big ( \mathbf{m}^{(k)} \big ) = \lambda_s \big ( \mathbf{m}^{(k-1)} \big ) - \bigg [ \Big ( \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big )^2 + \epsilon^2 \bigg ]^{p/2 - 1} - - where :math:`\epsilon` is a small constant added for stability of the algorithm - (set using `irls_threshold`). :math:`\lambda_s (\mathbf{m})` is an optional scaling - constant (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse - norm has roughly the same magnitude as the equivalent 2-norm for the same model. - The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, - where - - .. math:: - \mathbf{R} = \textrm{diag} \Big ( \mathbf{r_s}^{\! 1/2} \Big ) - - **Custom weights and the weighting matrix:** + **IRLS weights, user-defined weighting and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the discrete regularization function + custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights + for iteration :math:`k`. The net weighting applied within the discrete regularization function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. + For a description of how IRLS weights are updated at every iteration, see the documentation + for :py:meth:`update_weights`. + The weighting matrix used to apply the weights is given by: .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) ``numpy.ndarray``. The weights can be set all at once during instantiation @@ -326,17 +343,58 @@ class SparseSmallness(BaseSparse, Smallness): _multiplier_pair = "alpha_s" def update_weights(self, m): - r"""Compute and update the IRLS weights. - - The weights used to construct the re-weighting matrix :math:`\mathbf{R}` for - sparse-norm inversion are model-dependent and must be updated at every IRLS iteration. - This method recomputes and stores the weights. For a comprehensive description, - see the *Notes* section in the :class:`SparseSmallness` class documentation. + r"""Update the IRLS weights for sparse smallness regularization. Parameters ---------- m : numpy.ndarray - The model. + The model used to update the IRLS weights. + + Notes + ----- + For the model :math:`\mathbf{m}` provided, the least-squares regularization kernel + for sparse smallness is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + where :math:`\mathbf{m}^{(ref)}` is the reference model; see :py:meth:`Smallness.f_m` + for a more comprehensive definition. + + The IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the norm for each cell (defined using the `norm` property). + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the + regularization, and improves convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To compute the scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\;\;\;\, for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 + \end{cases} + + The scaling quantity :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] + \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2 \bigg ]^{1 - \mathbf{p}/2} """ f_m = self.f_m(m) self.set_weights(irls=self.get_lp_weights(f_m)) @@ -360,8 +418,14 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): define the simulation. orientation : {'x','y','z'} The direction along which sparse smoothness is applied. - norm : float - The norm used in the regularization function. Must be within theinterval [0, 2]. + norm : float, array_like + The norm defining sparseness thoughout the regularization function. Must be within the + interval [0,2]. There are several options: + + - ``float``: constant sparse norm throughout the domain. + - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). + - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). + active_cells : None, numpy.ndarray of bool Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. @@ -399,11 +463,13 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \bigg | \frac{\partial m}{\partial x} \bigg |^p \, dv + \bigg | \frac{\partial m}{\partial x} \bigg |^{p(r)} \, dv - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. - The parameter :math:`p \in [0,2]` defines the `norm`, where a smaller norm is used to - recovery structures with sharper boundaries. + where :math:`m(r)` is the model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. Sharper boundaries are recovered in regions + where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discrete approximation for the regularization @@ -411,11 +477,12 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i - \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^p + \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^{p_i} - where :math:`m_i \in \mathbf{m}` are discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that account - for cell dimensions in the discretization and apply user-defined weighting. + where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each face (set using `norm`). It is impractical to work with the general form directly, as its derivatives with respect to the model are non-linear and discontinuous. Instead, the iteratively re-weighted @@ -425,7 +492,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \gamma \big (\mathbf{m}^{(k)} \big ) = \frac{1}{2} \sum_i - \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^p + \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^{p_i} \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^2 @@ -433,92 +500,66 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial m_i^{(k-1)}}{\partial x} \Bigg )^2 + - \epsilon^2 \; \Bigg ]^{p/2 - 1} + \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} - and :math:`\epsilon` is a small constant added for stability. For the set of model parameters - :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS - iteration :math:`k` can be expressed as follows: + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex + least-squares problem for IRLS iteration :math:`k` can be expressed as follows: .. math:: \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, - \mathbf{W \, R \, G_x} \, \mathbf{m}^{(k)} \Big \|^2 + \mathbf{W \, R}^{\!\! (k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 where - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), - - :math:`\mathbf{R}` is the IRLS re-weighting matrix, and - - :math:`\mathbf{W}` is the weighting matrix. - - Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, :math:`\mathbf{R}` and - :math:`\mathbf{W}` are operators that act on variables living on x-faces. - - **IRLS weights and the re-weighting matrix:** + - :math:`\mathbf{W}^{(k)` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. - For smoothness along the x-direction, the IRLS weights :math:`\mathbf{r_x}` at every iteration - are recomputed internally using the previous iteration via: - - .. math:: - \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda_x \big ( \mathbf{m}^{(k-1)} \big ) - \bigg [ \Big ( \mathbf{G_x} \big [ \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \big ] \Big )^2 - + \epsilon^2 \bigg ]^{q_x/2 - 1} - - where :math:`\epsilon` is a small constant added for stability of the algorithm - (set using `irls_threshold`) and :math:`\lambda_x (\mathbf{m})` is an optional scaling constant - (``bool`` set with `irls_scaled`). The scaling constant ensures the sparse - norm has roughly the same magnitude as the equivalent 2-norm for the same model. - The weights are then used to construct the IRLS re-weighting matrix :math:`\mathbf{R}`, where - - .. math:: - \mathbf{R} = \textrm{diag} \Big ( \mathbf{r_x}^{\! 1/2} \Big ) - - Likewise for smoothness along y or z. + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix + acts on variables living on x-faces. **Reference model in smoothness:** - Gradients/interfaces within a discrete reference model :math:`m^{(ref)}` can be preserved by - including the reference model the smoothness regularization function. For smoothness along - the x-direction, the regularization function is given by: - - .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \bigg [ \frac{\partial}{\partial x} \Big ( m - m^{(ref)} \Big ) \bigg ]^p \, dv - - When discretized onto a mesh, the regularization function becomes: + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization function. + In this case, the discrete regularization function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - And the IRLS weights are at iteration :math:`k` are given by: + The least-squares problem for IRLS iteration :math:`k` becomes: .. math:: - \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda \big ( \mathbf{m}^{(k-1)} \big ) - \Bigg [ \bigg ( \mathbf{G_x} \Big [ \mathbf{m}^{(k-1)} - \mathbf{m}^{(ref)} \Big ] \bigg )^2 - + \epsilon^2 \bigg ]^{p/2 - 1} + \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W}^{\! (k)} \mathbf{G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. - **Custom weights and the weighting matrix:** + **IRLS weights, user-defined weighting and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined directly on the - faces or have been averaged from cell centers. - - The weighting applied within the discrete regularization function is given by: + faces or have been averaged from cell centers. And let :math:`\mathbf{r_x}^{\!\! (k)}` represent + the IRLS weights for iteration :math:`k`. The net weighting applied within the discrete + regularization function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \tilde{v} \odot \prod_i \mathbf{w_i} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized onto the mesh. - The weighting matrix is given by: + and dimensions when the regularization function is discretized to the mesh. + For a description of how IRLS weights are updated at every iteration, see the documentation + for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are @@ -547,17 +588,73 @@ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): super().__init__(mesh=mesh, orientation=orientation, **kwargs) def update_weights(self, m): - r"""Compute and update the IRLS weights. - - The weights used to construct the re-weighting matrix :math:`\mathbf{R}` for - sparse-norm inversion are model-dependent and must be updated at every IRLS iteration. - This method recomputes and stores the weights. For a comprehensive description, - see the *Notes* section in the :class:`SparseSmoothness` class documentation. + r"""Update the IRLS weights for sparse smoothness regularization. Parameters ---------- m : numpy.ndarray - The model. + The model used to update the IRLS weights. + + Notes + ----- + Let us consider the IRLS weights for sparse smoothness along the x-direction. + When the class property `gradient_type`=`'components'`, IRLS weights are computed + using the least-squares regularization kernel and we define: + + .. math:: + \mathbf{f_m} = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + where :math:`\mathbf{m}` is the model provided, :math:`\mathbf{G_x}` is the partial cell + gradient operator along x (i.e. x-derivative), and :math:`\mathbf{m}^{(ref)}` is a + reference model (optional, activated using `reference_model_in_smooth`). + See :py:meth:`Smoothness.f_m` for a more comprehensive definition of the least-squares + regularization kernel. + + However, when the class property `gradient_type`=`'total'`, IRLS weights are computed + using the magnitude of the total gradient and we define: + + .. math:: + \mathbf{{f}_m} = \mathbf{A_{cx}} \sum_{j=x,y,z} \Big | \mathbf{A_j G_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big | + + where :math:`\mathbf{A_j}` for :math:`j=x,y,z` averages the partial gradients from their + respective faces to cell centers, and :math:`\mathbf{A_{cx}}` averages the sum of the + absolute values back to the appropriate faces. + + Once :math:`\mathbf{f_m}` is obtained, the IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the norm for each element (set using the `norm` property). + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the model + objective function, and improves convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To apply the scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\;\;\;\, for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 + \end{cases} + + The scaling vector :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] + \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2 \bigg ]^{1 - \mathbf{p}/2} """ if self.gradient_type == "total": delta_m = self.mapping * self._delta_m(m) @@ -593,12 +690,18 @@ def update_weights(self, m): @property def gradient_type(self) -> str: - """Gradient measure used in the IRLS re-weighting. + """Gradient measure used to update IRLS weights for sparse smoothness. + + This property specifies whether the IRLS weights for sparse smoothness regularization + are updated using the total gradient (*"total"*) or using the partial gradient along + the smoothing orientation (*"components"*). To see how the IRLS weights are computed, + visit the documentation for :py:meth:`update_weights`. Returns ------- str in {"total", "components"} - Whether to re-weight using the total gradient or components of the gradient. + Whether to re-weight using the total gradient or partial gradients along + smoothing orientations. """ return self._gradient_type @@ -678,27 +781,33 @@ class Sparse(WeightedLeastSquares): .. math:: \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) - \Big [ m(r) - m^{(ref)}(r) \Big ]^{p_s} \, dv + \Big [ m(r) - m^{(ref)}(r) \Big ]^{p_s(r)} \, dv + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) - \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{q_j} \, dv + \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{p_j(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. Constants :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` weight the respective contributions of the smallness and smoothness regularization functions. - :math:`p_s` is the norm for the smallness term and :math:`q_j` for :math:`j=x,y,z` - are the norms for the smoothness terms. + :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness throughout the recovered + model; where more compact structures are recovered in regions where :math:`p_s(r)` is small. + And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which impose sparse smoothness + throughout the recovered model along the specified direction; where sharper boundaries are + recovered in regions where these parameters are small. For implementation within SimPEG, regularization functions and their variables - must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose - discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate - as follows: + must be discretized onto a `mesh`. For a regularization function whose kernel is given by + :math:`f(r)`, we approximate as follows: .. math:: - \int_\Omega w(r) \big [ x(r) \big ]^p \, dv \approx \sum_i \tilde{w}_i \, | x_i |^p + \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} - where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions - in the discretization and apply user-defined weighting. + where :math:`f_i \in \mathbf{f_m}` define the discrete least-squares regularization kernel on the mesh; + e.g. for smallness regularization, the least-squares regularization kernel is + :math:`\mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)}`. + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). It is impractical to work with sparse norms directly, as their derivatives with respect to the model are non-linear and discontinuous. Instead, the iteratively re-weighted @@ -706,82 +815,59 @@ class Sparse(WeightedLeastSquares): a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: .. math:: - \sum_i \tilde{w}_i \, \Big | x_i^{(k)} \Big |^p - \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | x_i^{(k)} \Big |^2 + \sum_i \tilde{w}_i \, \Big | f_i^{(k)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | f_i^{(k)} \Big |^2 - where IRLS weight :math:`r_i` for iteration :math:`k` is obtained from the previous iteration via: + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: .. math:: - r_i^{(k)} = \bigg [ \Big ( x_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p/2 - 1} + r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} - and :math:`\epsilon` is a small constant added for stability. For the set of model parameters - :math:`\mathbf{m}` defined at cell centers, the convex least-squares problem for IRLS - iteration :math:`k` can be expressed as follows: + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex + least-squares problem for IRLS iteration :math:`k` can be expressed as follows: .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} - \Big \| \mathbf{W_s R_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j R_j G_j m} ] \Big \|^2 + \Big \| \mathbf{W_s}^{\!\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j \, m} \Big \|^2 where - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), - :math:`\mathbf{m}^{(ref)}` is the reference model, - - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, - - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices, and - - :math:`\mathbf{R_s, \, R_x, \, R_y, \; R_z}` are IRLS re-weighting matrices + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, and + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are the weighting matrices for iteration :math:`k`. - **Alphas and length scales:** - - The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness - terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an - appropriate property of the ``Sparse`` class; e.g. :math:`\alpha_x` is set - using the `alpha_x` property. Note that unless the parameters are set manually, second-order - smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` - and `alpha_zz` properties are set to 0 by default. + The weighting matrices apply the IRLS weights, user-defined weighting, and account for cell + dimensions when the regularization functions are discretized. - The model objective function has been formulated such that each term is - roughly the same size when the :math:`\alpha` parameters are equal; e.g. when - :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying - a default weighting to each term which negates the effects of cell size/dimensions. - - Smoothness parameters can also be set using length scales. For example, by setting the - `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: - - >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 - - and - - >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 - - Likewise for y and z. - - **Custom weights and weighting matrices:** + **IRLS weights, user-defined weighting and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights that are applied to all regularization terms in the model objective function. - The general form for the weights applied to the smallness term is given by: + For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness + term is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{\tilde{v}_s} \odot \prod_j \mathbf{w_j} - And the general form for the weights applied to first-order smoothness terms is given by: + And for sparse smoothness along x (likewise for y and z) is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{P \, w_k} + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{\tilde{v}_x} \odot \prod_j \mathbf{P_x \, w_j} - :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell volumes - and dimensions when regularization functions are discretized onto the mesh. These default - weights vary depending on the regularization term; e.g. smallness, first-order smoothness in - the y-direction. For first-order smoothness terms, a projection matrix :math:`\mathbf{P}` is - required to average weights at cell centers to the appropriate faces; i.e. where discrete - first-order derivatives live. + The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` + for :math:`\ast = s,x,y,z`. The default weights that account for cell dimensions when the + regularization functions are discretized are defined as :math:`\mathbf{\tilde{v}_\ast}` + for :math:`\ast = s,x,y,z`. Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` + project the user-defined weighting to the appropriate faces. - Weights for each regularization term are used to construct their respective weighting matrices - as follows: + Once the net weights for all terms in the model objective function are computed, + their weighting matrices can be constructed via: .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + \mathbf{W}_\ast^{(k)} = \textrm{diag} \Big ( \, \sqrt{\mathbf{w_\ast}^{\!\! (k)} \, } \Big ) Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) ``numpy.ndarray``. The weights can be set all at once during instantiation @@ -793,35 +879,46 @@ class Sparse(WeightedLeastSquares): >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - **IRLS weights and the re-weighting matrix:** + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model can be preserved by including the + reference model the smoothness regularization functions. In this case, + the discrete regularization function becomes: + + .. math:: + \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \Big \| \mathbf{W_s}^{\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| + \mathbf{W_j}^{\! (k)} \mathbf{G_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - IRLS weights allow each term in the sparse regularization to be approximated by a weighted 2-norm. - The IRLS weights for each term are model-dependent and must be computed at every IRLS iteration. - For iteration :math:`k`, the IRLS weights :math:`\mathbf{r_s}` for the smallness term - are updated internally using the previous model via: + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. - .. math:: - \mathbf{r_s} \big ( \mathbf{m}^{(k)} \big ) = \lambda_s \big ( \mathbf{m}^{(k-1)} \big ) - \bigg [ \Big ( \big ( \mathbf{m}^{(k-1)} \big ) \Big )^2 + \epsilon^2 \bigg ]^{p_s/2 - 1} + **Alphas and length scales:** - For first-order smoothness along the x-direction (likewise for smoothness in y and z), - the IRLS weights :math:`\mathbf{r_x}` are updated via: + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``Sparse`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` properties are set to 0 by default. - .. math:: - \mathbf{r_x} \big ( \mathbf{m}^{(k)} \big ) = \lambda_x \big ( \mathbf{m}^{(k-1)} \big ) - \bigg [ \Big ( \big ( \mathbf{G_x m}^{(k-1)} \big ) \Big )^2 + \epsilon^2 \bigg ]^{q_x/2 - 1} + The model objective function has been formulated such that each term is + roughly the same size when the :math:`\alpha` parameters are equal; e.g. when + :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying + a default weighting to each term which negates the effects of cell size/dimensions. - where :math:`\epsilon` is a small constant added for stability of the algorithm - (set using `irls_threshold`). :math:`\lambda` are optional scaling constants - (``bool`` set with `irls_scaled`) applied to each set of IRLS weights. - The scaling constant ensures the sparse norm has roughly the same magnitude as the - equivalent 2-norm for the same model. The IRLS weights are then used to construct their - respective re-weighting matrices :math:`\mathbf{R_\ast}` as follows: + Smoothness parameters can also be set using length scales. For example, by setting the + `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: - .. math:: - \mathbf{R_\ast} = \textrm{diag} \Big ( \mathbf{r_\ast}^{\! 1/2} \Big ) + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. - where :math:`\ast` represents :math:`s, x, y` or :math:`z`. """ def __init__( @@ -882,12 +979,18 @@ def __init__( @property def gradient_type(self) -> str: - """Choice of gradient measure for IRLS weights. + """Gradient measure used to update IRLS weights for sparse smoothness. + + This property specifies whether the IRLS weights for sparse smoothness regularization(s) + terms are updated using the total gradient (*"total"*) or using the partial gradients along + their smoothing orientations (*"components"*). To see how the IRLS weights are computed, + visit the documentation for :py:meth:`~SparseSmoothness.update_weights`. Returns ------- - {"total", "component"} - Choice of gradient measure for IRLS weights. + str in {"total", "components"} + Whether to re-weight using the total gradient or partial gradients along + smoothing orientations. """ return self._gradient_type @@ -907,7 +1010,7 @@ def gradient_type(self, value: str): def norms(self): """Norms for the child regularization classes. - Supply the norms for the smallness and all smoothness terms in the ``Sparse`` regularization. + Norms for the smallness and all smoothness terms in the ``Sparse`` regularization. Returns ------- @@ -979,12 +1082,19 @@ def irls_threshold(self, value): self._irls_threshold = value def update_weights(self, model): - """Trigger IRLS update for all child regularization functions. + """Update IRLS weights for all child regularization objects. + + For an instance of the `Sparse` regularization class, this method re-computes and updates + the IRLS for all child regularization objects using the model provided. + To see how IRLS weights are recomputed for :class:`SparseSmallness` objects, visit the + documentation for :py:meth:`SparseSmallnes.update_weights`. And for + :class:`SparseSmoothness` objects, visit the documentation for + :py:meth:`SparseSmoothness.update_weights`. Parameters ---------- - model - The model. + model : (n_params, ) numpy.ndarray + The model used to recompute the IRLS weights. """ for fct in self.objfcts: fct.update_weights(model) From 1cf25b58660be1dbb44513d699f1e4b9090a632a Mon Sep 17 00:00:00 2001 From: yanang007 Date: Mon, 22 May 2023 01:34:32 +0800 Subject: [PATCH 080/455] replace deprecated numpy aliases with builtin types --- .../viscous_remanent_magnetization/sources.py | 10 +++++----- SimPEG/utils/drivers/gravity_driver.py | 8 ++++---- SimPEG/utils/drivers/magnetics_driver.py | 8 ++++---- SimPEG/utils/io_utils/io_utils_general.py | 4 ++-- examples/20-published/plot_heagyetal2017_casing.py | 2 +- examples/20-published/plot_schenkel_morrison_casing.py | 2 +- tests/em/nsem/inversion/test_BC_Sims.py | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py b/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py index d438e26a6e..34a660862e 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py +++ b/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py @@ -168,7 +168,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - refFlag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + refFlag = np.zeros(np.shape(xyzc)[0], dtype=int) r = np.sqrt( (xyzc[:, 0] - self.location[0]) ** 2 @@ -330,7 +330,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - refFlag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + refFlag = np.zeros(np.shape(xyzc)[0], dtype=int) r0 = self.location a = self.radius @@ -523,12 +523,12 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - ref_flag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + ref_flag = np.zeros(np.shape(xyzc)[0], dtype=int) nSeg = np.shape(self.location)[0] - 1 for tt in range(0, nSeg): - ref_flag_tt = np.zeros(np.shape(xyzc)[0], dtype=np.int) + ref_flag_tt = np.zeros(np.shape(xyzc)[0], dtype=int) tx0 = self.location[tt, :] tx1 = self.location[tt + 1, :] a = (tx1[0] - tx0[0]) ** 2 + (tx1[1] - tx0[1]) ** 2 + (tx1[2] - tx0[2]) ** 2 @@ -546,7 +546,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): + (tx0[2] - xyzc[:, 2]) ** 2 - d**2 ) - e = np.array(b**2 - 4 * a * c, dtype=np.complex) + e = np.array(b**2 - 4 * a * c, dtype=complex) q_pos = (-b + np.sqrt(e)) / (2 * a) q_neg = (-b - np.sqrt(e)) / (2 * a) diff --git a/SimPEG/utils/drivers/gravity_driver.py b/SimPEG/utils/drivers/gravity_driver.py index b63354bfe6..2d25574212 100644 --- a/SimPEG/utils/drivers/gravity_driver.py +++ b/SimPEG/utils/drivers/gravity_driver.py @@ -110,7 +110,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:5]) - alphas = val.astype(np.float) + alphas = val.astype(float) elif l_input[0] == "DEFAULT": alphas = np.ones(4) @@ -120,7 +120,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - bounds = val.astype(np.float) + bounds = val.astype(float) elif l_input[0] == "FILE": bounds = l_input[1].rstrip() @@ -133,7 +133,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:6]) - lpnorms = val.astype(np.float) + lpnorms = val.astype(float) elif l_input[0] == "FILE": lpnorms = l_input[1].rstrip() @@ -143,7 +143,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - eps = val.astype(np.float) + eps = val.astype(float) elif l_input[0] == "DEFAULT": eps = None diff --git a/SimPEG/utils/drivers/magnetics_driver.py b/SimPEG/utils/drivers/magnetics_driver.py index 4f7b884866..a6f2455193 100644 --- a/SimPEG/utils/drivers/magnetics_driver.py +++ b/SimPEG/utils/drivers/magnetics_driver.py @@ -119,7 +119,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:5]) - alphas = val.astype(np.float) + alphas = val.astype(float) elif l_input[0] == "DEFAULT": alphas = np.ones(4) @@ -129,7 +129,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - bounds = val.astype(np.float) + bounds = val.astype(float) elif l_input[0] == "FILE": bounds = l_input[1].rstrip() @@ -142,7 +142,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:6]) - lpnorms = val.astype(np.float) + lpnorms = val.astype(float) elif l_input[0] == "FILE": lpnorms = l_input[1].rstrip() @@ -152,7 +152,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - eps = val.astype(np.float) + eps = val.astype(float) elif l_input[0] == "DEFAULT": eps = None diff --git a/SimPEG/utils/io_utils/io_utils_general.py b/SimPEG/utils/io_utils/io_utils_general.py index c167bc4594..2fddb56243 100644 --- a/SimPEG/utils/io_utils/io_utils_general.py +++ b/SimPEG/utils/io_utils/io_utils_general.py @@ -35,7 +35,7 @@ def read_GOCAD_ts(tsfile): while re.match("VRTX", line): l_input = re.split(r"[\s*]", line) temp = np.array(l_input[2:5]) - vrtx.append(temp.astype(np.float)) + vrtx.append(temp.astype(float)) # Read next line line = fid.readline() @@ -53,7 +53,7 @@ def read_GOCAD_ts(tsfile): while re.match("TRGL", line): l_input = re.split(r"[\s*]", line) temp = np.array(l_input[1:4]) - trgl.append(temp.astype(np.int)) + trgl.append(temp.astype(int)) # Read next line line = fid.readline() diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index e37f06be34..58d76f70ab 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -184,7 +184,7 @@ def meshp(self): # cell size, number of core cells, number of padding cells in the # x-direction - ncz = np.int(np.ceil(np.diff(self.casing_z)[0] / csz)) + 10 + ncz = int(np.ceil(np.diff(self.casing_z)[0] / csz)) + 10 npadzu, npadzd = 43, 43 # vector of cell widths in the z-direction diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index c1410a3f7d..2511751138 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -105,7 +105,7 @@ def run(plotIt=True): nza = 10 # cell size, number of core cells, number of padding cells in the # x-direction - ncz, npadzu, npadzd = np.int(np.ceil(np.diff(casing_z)[0] / csz)) + 10, 68, 68 + ncz, npadzu, npadzd = int(np.ceil(np.diff(casing_z)[0] / csz)) + 10, 68, 68 # vector of cell widths in the z-direction hz = utils.unpack_widths([(csz, npadzd, -1.3), (csz, ncz), (csz, npadzu, 1.3)]) diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index 42602a2f32..c1063656c4 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -163,7 +163,7 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): right = np.where(b_e[:, 0] == mesh.nodes_x[-1]) h_bc = {} for src in survey_1d.source_list: - h_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=np.complex) + h_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=complex) h_bc_freq[top] = 1.0 h_bc_freq[right] = f_right[src, "h"][:, 0] h_bc_freq[left] = f_left[src, "h"][:, 0] @@ -214,7 +214,7 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): right = np.where(b_e[:, 0] == mesh.nodes_x[-1]) e_bc = {} for src in survey_1d.source_list: - e_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=np.complex) + e_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=complex) e_bc_freq[top] = 1.0 e_bc_freq[right] = f_right[src, "e"][:, 0] e_bc_freq[left] = f_left[src, "e"][:, 0] From 6971b50f03b2b4d0343a00c1609578445a14b8a1 Mon Sep 17 00:00:00 2001 From: dccowan Date: Tue, 23 May 2023 09:28:01 -0700 Subject: [PATCH 081/455] small edits --- SimPEG/regularization/__init__.py | 89 ++--- SimPEG/regularization/base.py | 382 ++++++++++--------- SimPEG/regularization/regularization_mesh.py | 80 ++-- SimPEG/regularization/sparse.py | 186 ++++----- 4 files changed, 407 insertions(+), 330 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index a53b649d69..1e524654e4 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -5,72 +5,74 @@ .. currentmodule:: SimPEG.regularization -``Regularization`` classes are used to impose constraints on models recovered through geophysical inversion. -Constraints may be straight forward, such as: setting upper and lower bounds for values in the recovered model, -requiring the recovered model be spatially smooth, or using a reference model to add a-priori information. -Constraints may also be more sophisticated; e.g. cross-validation and petrophysically-guided regularization. -In SimPEG, constraints on the recovered model can be defined using a single ``Regularization`` object, -or defined by combining multiple ``Regularization`` objects. +``Regularization`` classes are used to impose constraints on models recovered through geophysical +inversion. Constraints may be straight forward, such as: requiring the recovered model be +spatially smooth, or using a reference model to add a-priori information. Constraints may also +be more sophisticated; e.g. cross-validation and petrophysically-guided regularization. +In SimPEG, constraints on the recovered model can be defined using a single ``Regularization`` +object, or defined as a weighted sum of ``Regularization`` objects. Basic Theory ------------ -Most geophysical inverse problems suffer from non-uniqueness; i.e. there is an infinite number of models -(:math:`m`) capable of reproducing the observed data to within a specified degree of uncertainty. -The challenge is recovering a model which 1) reproduces the observed data, and 2) reasonably approximates -the subsurface structures responsible for the observed geophysical response. To accomplish this, -regularization functions are used to ensure the solution to the inverse problem is unique and is -geologically plausible. The choice in regularization function(s) depends on user assumptions and -a priori information. +Most geophysical inverse problems suffer from non-uniqueness; i.e. there is an infinite number +of models (:math:`m`) capable of reproducing the observed data to within a specified +degree of uncertainty. The challenge is recovering a model which 1) reproduces the observed data, +and 2) reasonably approximates the subsurface structures responsible for the observed geophysical +response. To accomplish this, regularization is used to ensure the solution to the inverse +problem is unique and is geologically plausible. The regularization applied to solve the inverse +problem depends on user assumptions and a priori information. -SimPEG uses a deterministic inversion approach to recover an appropriate model. The algorithm does this -by finding the model (:math:`m`) which minimizes a global objective function (or penalty function) of the form: +SimPEG uses a deterministic inversion approach to recover an appropriate model. +The algorithm does this by finding the model (:math:`m`) which minimizes a global objective +function (or penalty function) of the form: .. math:: \phi (m) = \phi_d (m) + \beta \, \phi_m (m) The global objective function contains two terms: a data misfit term :math:`\phi_d` which -ensures data predicted by the recovered model adequately reproduces the observed data, and the model -objective function :math:`\phi_m` which is comprised of one or more regularization functions :math:`\gamma_i (m)`. I.e.: +ensures data predicted by the recovered model adequately reproduces the observed data, +and the model objective function :math:`\phi_m` which is comprised of one or more +regularization functions :math:`\gamma_i (m)`. I.e.: .. math:: \phi_m (m) = \sum_i \alpha_i \, \gamma_i (m) The model objective function imposes all of the desired constraints on the recovered model. -Constants :math:`\alpha_i` weight the relative contributions of the regularization functions -comprising the model objective function. The trade-off parameter :math:`\beta` balances the -relative contribution of the data misfit and regularization functions on the global objective function. +Constants :math:`\alpha_i` weight the relative contributions of the regularization +functions comprising the model objective function. The trade-off parameter :math:`\beta` +balances the relative contribution of the data misfit and regularization functions on the +global objective function. -Regularization classes within SimPEG correspond to different regularization functions that can be -used individually or combined to define the model objective function :math:`\phi_m (\mathbf{m})`. -For example, a combination of regularization functions that ensures the values in the recovered -model are not too large and are spatially smooth in the x and y-directions can be expressed as: +Regularization classes within SimPEG correspond to different regularization (objective) +functions that can be used individually or combined to define the model objective function +:math:`\phi_m (\mathbf{m})`. For example, a combination of regularization functions that ensures +the values in the recovered model are not too large and are spatially smooth in the x and +y-directions can be expressed as: .. math:: \phi_m (m) = \alpha_s \! \int_\Omega \Bigg [ \frac{1}{2} w_s(r) \, m(r)^2 \Bigg ] \, dv + - \alpha_x \! \int_\Omega \Bigg [ \frac{1}{2} w_x(r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + - \alpha_y \! \int_\Omega \Bigg [ \frac{1}{2} w_y(r) \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv + \alpha_x \! \int_\Omega \Bigg [ \frac{1}{2} w_x(r) + \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + + \alpha_y \! \int_\Omega \Bigg [ \frac{1}{2} w_y(r) + \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv where :math:`w_s(r), w_x(r), w_y(r)` are user-defined weighting functions. For practical implementation within SimPEG, the regularization function and all its dependent variables are discretized to a numerical grid (or mesh). The model is therefore defined as a discrete set of model parameters :math:`\mathbf{m}`. -And the regularization function is approximated by: +And the regularization is implemented using a weighted sum of objective functions: .. math:: - \begin{align} - \phi_m (\mathbf{m}) &\approx \frac{\alpha_s}{2} \mathbf{m^T W_s^T W_s m} + - \frac{\alpha_x}{2} \mathbf{m^T G_x^T W_x^T W_x G_x m} + - \frac{\alpha_y}{2} \mathbf{m^T G_y^T W_y^T W_y G_y m} \\ - &\approx \frac{1}{2} \mathbf{m^T R^T R m} - \end{align} + \phi_m (\mathbf{m}) \approx \frac{\alpha_s}{2} \big \| \mathbf{W_s m} \big \|^2 + + \frac{\alpha_x}{2} \big \| \mathbf{W_x G_x m} \big \|^2 + + \frac{\alpha_y}{2} \big \| \mathbf{W_y G_y m} \big \|^2 -where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradients along the x and y-directions, respectively. -:math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` are weighting matrices that apply -user-defined weights and account for cell dimensions in the integration. -As is the case with multiple least-squares regularization functions, the terms can be amalgamated -and used to define the model objective function using a single regularization operator :math:`\mathbf{R}`. +where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradient operators along the x and +y-directions, respectively. :math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` +are weighting matrices that apply user-defined weights and account for cell dimensions +in the discretization. The API @@ -78,20 +80,20 @@ Weighted Least Squares Regularization ------------------------------------- -Weighted least squares regularization functions are defined as weighted L2-norms on the model, its first-order -directional derivative(s), or its second-order directional derivative(s). +Weighted least squares regularization functions are defined as weighted L2-norms on the model, +its first-order directional derivative(s), or its second-order directional derivative(s). .. autosummary:: :toctree: generated/ + WeightedLeastSquares Smallness SmoothnessFirstOrder SmoothnessSecondOrder - WeightedLeastSquares Sparse Norm Regularization -------------------------- -Sparse norm regularization functions allow for the recovery of compact and/or blocky structures. +Sparse norm regularization allows for the recovery of compact and/or blocky structures. An iteratively re-weighted least-squares approach allows smallness and smoothness regularization functions to be defined using norms between 0 and 2. @@ -117,7 +119,8 @@ Base Regularization Classes --------------------------- -Base regularization classes. Inherited by other classes and not used directly to constrain inversions. +Base regularization classes. Inherited by other classes and not used directly +to constrain inversions. .. autosummary:: :toctree: generated/ diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index d2186be31d..5fc1569f70 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -16,10 +16,10 @@ class BaseRegularization(BaseObjectiveFunction): - """Base class for least-squares regularization. + """Base regularization class. The ``BaseRegularization`` class defines properties and methods inherited by - least-squares regularization classes. It is not directly used to constrain + SimPEG regularization classes. It is not directly used to constrain the inversions. Parameters @@ -27,15 +27,15 @@ class BaseRegularization(BaseObjectiveFunction): mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the - regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the - reference model is equal to the starting model for the inversion. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -89,18 +89,18 @@ def active_cells(self) -> np.ndarray: """Active cells defined on the regularization mesh. A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` - that are active throughout the inversion. Inactive cells remain fixed and are defined - according to the starting model. + that are active (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. Returns ------- - (n_cells, ) Array of bool + (n_cells, ) array of bool Notes ----- - If the property is set using an array of integers, the setter interprets the array as - representing the indices of the active cells. When called however, the quantity will have - been converted to a boolean array. + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. """ return self.regularization_mesh.active_cells @@ -217,8 +217,9 @@ def reference_model(self) -> np.ndarray: Returns ------- - (n_param, ) numpy.ndarray - Reference model. + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. """ return self._reference_model @@ -491,8 +492,9 @@ class Smallness(BaseRegularization): model values in the recovered model and the reference model are small; i.e. it preserves structures in the reference model. If a reference model is not supplied, the starting model will be set as the reference model in the - regularization by default. Optionally, custom cell weights can be included to - control the degree of smallness being enforced throughout different regions the model. + corresponding objective function by default. Optionally, custom cell weights can be + included to control the degree of smallness being enforced throughout different + regions the model. See the *Notes* section below for a comprehensive description. @@ -501,15 +503,15 @@ class Smallness(BaseRegularization): mesh : .regularization.RegularizationMesh Mesh on which the regularization is discretized. Not the mesh used to define the simulation. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, .maps.IdentityMap - The mapping from the model parameters to the quantity defined in the regularization. - If ``None``, the mapping is the :class:`.maps.IdentityMap`. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting model - is set as the reference model in the inversion. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -523,14 +525,15 @@ class Smallness(BaseRegularization): We define the regularization function for smallness as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w( \mathscr{r}) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv + \gamma (m) = \frac{1}{2} \int_\Omega \, w( \mathscr{r}) \, + \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` is a user-defined weighting function. For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function is given by: + function (objective function) is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i @@ -538,8 +541,8 @@ class Smallness(BaseRegularization): where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of - linear operators, the discrete regularization function can be expressed as: + for cell dimensions in the discretization and 2) apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} @@ -553,11 +556,10 @@ class Smallness(BaseRegularization): **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the discrete regularization function - is given by: + custom cell weights. The weighting applied within the objective function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. @@ -578,7 +580,7 @@ class Smallness(BaseRegularization): The default weights that account for cell dimensions in the regularization are accessed via: - >>> reg.get_weights['volume'] + >>> reg.get_weights('volume') """ @@ -611,8 +613,7 @@ def f_m(self, m) -> np.ndarray: Notes ----- - The discretized approximation for the smallness regularization function is given by - (see documentation for the :class:`Smallness` class for further reading): + The objective function for smallness regularization is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -620,8 +621,9 @@ def f_m(self, m) -> np.ndarray: where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + the weighting matrix. See the :class:`Smallness` class documentation for more detail. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -657,17 +659,17 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized approximation for the smallness regularization function is given by - (see documentation for the :class:`Smallness` class for further reading): + The objective function for smallness regularization is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - where :math:`\mathbf{m}` are the discrete model parameters (model), + where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. We define the least-squares regularization kernel - :math:`\mathbf{f_m}` as: + the weighting matrix. See the :class:`Smallness` class documentation for more detail. + + We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -705,16 +707,16 @@ class SmoothnessFirstOrder(BaseRegularization): The mesh on which the regularization is discretized. orientation : {'x', 'y', 'z'} The direction along which smoothness is enforced. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, .maps.IdentityMap - The mapping from the model parameters to the quantity defined in the regularization. - If ``None``, the mapping is the :class:`.maps.IdentityMap`. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting model - is set as the reference model by default. To include the reference model in the - regularization function, the `reference_model_in_smooth` property must be set to ``True``. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -740,19 +742,19 @@ class SmoothnessFirstOrder(BaseRegularization): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function is given by: + function (objective function) is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of - linear operators, the discrete regularization function can be expressed as: + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh + and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that + 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x m } \, \Big \|^2 + \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, G_x m } \, \Big \|^2 where @@ -765,8 +767,8 @@ class SmoothnessFirstOrder(BaseRegularization): **Reference model in smoothness:** Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization function. - In this case, the discrete regularization function becomes: + preserved by including the reference model the regularization. + In this case, the objective function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} @@ -783,10 +785,10 @@ class SmoothnessFirstOrder(BaseRegularization): smoothness along the x-direction. Each set of weights were either defined directly on the faces or have been averaged from cell centers. - The weighting applied within the discrete regularization function is given by: + The weighting applied within the objective function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized onto the mesh. @@ -814,7 +816,7 @@ class SmoothnessFirstOrder(BaseRegularization): The default weights that account for cell dimensions in the regularization are accessed via: - >>> reg.get_weights['volume'] + >>> reg.get_weights('volume') """ @@ -909,8 +911,8 @@ def f_m(self, m): where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), :math:`\mathbf{m}` are the descrete model parameters defined on the - mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). Similarly for smoothness - along y and z. + mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). + Similarly for smoothness along y and z. Parameters ---------- @@ -924,9 +926,8 @@ def f_m(self, m): Notes ----- - The discretized approximation of the regularization function for first-order smoothness - along the x-direction is given by (see documentation for the - :class:`SmoothnessFirstOrder` class for further reading): + The objective function for first-order smoothness regularization along the x-direction + is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -936,6 +937,7 @@ def f_m(self, m): :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. + See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -980,9 +982,8 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized approximation of the regularization function for first-order smoothness - along the x-direction is given by (see documentation for the - :class:`SmoothnessFirstOrder` class for further reading): + The objective function for first-order smoothness regularization along the x-direction + is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} @@ -992,6 +993,7 @@ def f_m_deriv(self, m) -> csr_matrix: :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. + See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -1014,14 +1016,14 @@ def f_m_deriv(self, m) -> csr_matrix: def W(self): r"""Weighting matrix. - Returns the weighting matrix for the discrete regularization function. To see how the - weighting matrix is constructed, see the *Notes* section for the :class:`SmoothnessFirstOrder` - regularization class. + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`SmoothnessFirstOrder` regularization class. Returns ------- scipy.sparse.csr_matrix - The weighting matrix applied in the regularization. + The weighting matrix applied in the objective function. """ if getattr(self, "_W", None) is None: average_cell_2_face = getattr( @@ -1073,16 +1075,16 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): The mesh on which the regularization is discretized. orientation : {'x', 'y', 'z'} The direction along which smoothness is enforced. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, .maps.IdentityMap - The mapping from the model parameters to the quantity defined in the regularization. - If ``None``, the mapping is the :class:`.maps.IdentityMap`. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting model - is set as the reference model by default. To include the reference model in the - regularization function, the `reference_model_in_smooth` property must be set to ``True``. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -1105,16 +1107,16 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function is given by: + function (objective function) is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. In terms of - linear operators, the discrete regularization function can be expressed as: + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the + mesh and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that + 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 @@ -1128,7 +1130,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be preserved by including the reference model the smoothness regularization function. - In this case, the discrete regularization function becomes: + In this case, the objective function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} @@ -1141,11 +1143,11 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the discrete regularization function + custom cell weights. The weighting applied within the objective function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. @@ -1191,18 +1193,18 @@ def f_m(self, m): Notes ----- - The discretized approximation of the regularization function for second-order smoothness - along the x-direction is given by (see documentation for the - :class:`SmoothnessSecondOrder` class for further reading): + The objective function for second-order smoothness regularization along the x-direction + is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` is the - second order derivative operator with respect to x, and :math:`\mathbf{W}` is + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the + second-order x-derivative operator, and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. + See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -1229,8 +1231,8 @@ def f_m(self, m): def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the least-squares regularization kernel. - For second-order smoothness regularization, the derivative of the least-squares regularization - kernel with respect to the model is given by: + For second-order smoothness regularization, the derivative of the least-squares + regularization kernel with respect to the model is given by: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} @@ -1249,18 +1251,18 @@ def f_m_deriv(self, m) -> csr_matrix: Notes ----- - The discretized approximation of the regularization function for second-order smoothness - along the x-direction is given by (see documentation for the - :class:`SmoothnessSecondOrder` class for further reading): + The objective function for second-order smoothness regularization along the x-direction + is given by: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second - order derivative operator with respect to x, and :math:`\mathbf{W}` is + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the + second-order x-derivative operator, and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. + See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: @@ -1287,14 +1289,14 @@ def f_m_deriv(self, m) -> csr_matrix: def W(self): r"""Weighting matrix. - Returns the weighting matrix for the discrete regularization function. To see how the - weighting matrix is constructed, see the *Notes* section for the :class:`SmoothnessSecondOrder` - regularization class. + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`SmoothnessSecondOrder` regularization class. Returns ------- scipy.sparse.csr_matrix - The weighting matrix applied in the regularization. + The weighting matrix applied in the objective function. """ if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) @@ -1321,11 +1323,11 @@ class WeightedLeastSquares(ComboObjectiveFunction): and/or :class:`SmoothnessSecondOrder` (optional) least-squares regularization functions. ``Smallness`` regularization is used to ensure that values in the recovered model, or differences between the recovered model and a reference model, are not overly - large in magnitude. ``Smoothness`` regularizations are used to ensure that values in the - recovered model are smooth along specified directions. When `reference_in_smooth` is - used to include the reference model in the smoothness terms, the inversion preserves - gradients/interfaces within the reference model. The `weights` argument can be used - to supply custom weights to control the degree of smallness and smoothness being + large in magnitude. ``Smoothness`` regularization is used to ensure that values in the + recovered model are smooth along specified directions. When a reference model + is included in the smoothness regularization, the inversion preserves + gradients/interfaces within the reference model. Custom weights can also be supplied + to control the degree of smallness and smoothness being enforced throughout different regions the model. See the *Notes* section below for a comprehensive description. @@ -1335,15 +1337,15 @@ class WeightedLeastSquares(ComboObjectiveFunction): mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the - regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the - reference model is equal to the starting model for the inversion. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -1367,9 +1369,10 @@ class WeightedLeastSquares(ComboObjectiveFunction): Notes ----- - The model objective function :math:`\phi_m (m)` defined by the weighted sum of + Weighted least-squares regularization can be defined by a weighted sum of :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` - regularization functions is given by: + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: .. math:: \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) @@ -1382,10 +1385,11 @@ class WeightedLeastSquares(ComboObjectiveFunction): where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. - Constants :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` for :math:`j=x,y,z` - weight the respective contributions of the smallness and smoothness regularization functions. + Parameters :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` for :math:`j=x,y,z` + are multiplier constants which weight the respective contributions of the smallness and + smoothness terms towards the regularization. - For implementation within SimPEG, regularization functions and their variables + For implementation within SimPEG, the regularization functions and their variables must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate as follows: @@ -1395,8 +1399,8 @@ class WeightedLeastSquares(ComboObjectiveFunction): where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions in the discretization and apply user-defined weighting. Using the above approximation, - the discretized model objective function for ``WeightedLeastSquares`` regularization - can be expressed as: + the ``WeightedLeastSquares`` regularization can be expressed as a weighted sum of + objective functions of the form: .. math:: \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} @@ -1416,8 +1420,8 @@ class WeightedLeastSquares(ComboObjectiveFunction): **Reference model in smoothness:** Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization functions. - In this case, the discrete regularization function becomes: + preserved by including the reference model the smoothness regularization. + In this case, the objective function becomes: .. math:: \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} @@ -1442,8 +1446,8 @@ class WeightedLeastSquares(ComboObjectiveFunction): The model objective function has been formulated such that each term is roughly the same size when the :math:`\alpha` parameters are equal; e.g. when - :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying a default - weighting to each term which negates the effects of cell size/dimensions. + :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying a + default weighting to each term which negates the effects of cell size/dimensions. Smoothness parameters can also be set using length scales. For example, by setting the `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: @@ -1458,27 +1462,27 @@ class WeightedLeastSquares(ComboObjectiveFunction): **Custom weights and weighting matrices:** - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell - weights that are applied to all regularization terms in the model objective function. + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom + cell weights that are applied to all terms in the model objective function. The general form for the weights applied to smallness and second-order smoothness terms is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{w_k} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} and weights applied to first-order smoothness terms are given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_k \mathbf{P \, w_k} + \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{P \, w_j} - :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell volumes - and dimensions when regularization functions are discretized onto the mesh. These default - weights vary depending on the regularization term; e.g. smallness, first-order smoothness in - the y-direction. For first-order smoothness terms, a projection matrix :math:`\mathbf{P}` is - required to average weights at cell centers to the appropriate faces; i.e. where discrete - first-order derivatives live. + :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell + volumes and dimensions when regularization functions are discretized onto the mesh. + These default weights vary depending on the regularization term; e.g. smallness, first-order + smoothness in the y-direction. For first-order smoothness terms, a projection matrix + :math:`\mathbf{P}` is required to average weights at cell centers to the appropriate faces; + i.e. where discrete first-order derivatives live. - Weights for each regularization term are used to construct their respective weighting matrices + Weights for each term are used to construct their respective weighting matrices as follows: .. math:: @@ -1668,12 +1672,12 @@ def cell_weights(self, value): @property def alpha_s(self): - """Weighting constant for smallness term. + """Multiplier constant for the smallness term. Returns ------- float - Weighting constant for smallness term. + Multiplier constant for the smallness term. """ return self._alpha_s @@ -1691,12 +1695,12 @@ def alpha_s(self, value): @property def alpha_x(self): - """Weighting constant for first order x-derivative term. + """Multiplier constant for first-order smoothness along x. Returns ------- float - Weighting constant for first order x-derivative term. + Multiplier constant for first-order smoothness along x. """ return self._alpha_x @@ -1712,12 +1716,12 @@ def alpha_x(self, value): @property def alpha_y(self): - """Weighting constant for first order y-derivative term. + """Multiplier constant for first-order smoothness along y. Returns ------- float - Weighting constant for first order y-derivative term. + Multiplier constant for first-order smoothness along y. """ return self._alpha_y @@ -1733,12 +1737,12 @@ def alpha_y(self, value): @property def alpha_z(self): - """Weighting constant for first order z-derivative term. + """Multiplier constant for first-order smoothness along z. Returns ------- float - Weighting constant for first order z-derivative term. + Multiplier constant for first-order smoothness along z. """ return self._alpha_z @@ -1754,12 +1758,12 @@ def alpha_z(self, value): @property def alpha_xx(self): - """Weighting constant for second order x-derivative term. + """Multiplier constant for second-order smoothness along x. Returns ------- float - Weighting constant for second order x-derivative term. + Multiplier constant for second-order smoothness along x. """ return self._alpha_xx @@ -1777,12 +1781,12 @@ def alpha_xx(self, value): @property def alpha_yy(self): - """Weighting constant for second order y-derivative term. + """Multiplier constant for second-order smoothness along y. Returns ------- float - Weighting constant for second order y-derivative term. + Multiplier constant for second-order smoothness along y. """ return self._alpha_yy @@ -1800,12 +1804,12 @@ def alpha_yy(self, value): @property def alpha_zz(self): - """Weighting constant for second order z-derivative term. + """Multiplier constant for second-order smoothness along z. Returns ------- float - Weighting constant for second order z-derivative term. + Multiplier constant for second-order smoothness along z. """ return self._alpha_zz @@ -1823,12 +1827,19 @@ def alpha_zz(self, value): @property def length_scale_x(self): - """Constant multiplier of the base length scale on model gradients along x. + r"""Multiplier constant for smoothness along x relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_x` defines the multiplier constant for first-order smoothness along x, + the length-scale is given by: + + .. math:: + L_x = \bigg ( \frac{\alpha_x}{\Delta h} \bigg )^{1/2} Returns ------- float - Constant multiplier of the base length scale on model gradients along x. + Multiplier constant for smoothness along x relative to base scale length. """ return np.sqrt(self.alpha_x) / self.regularization_mesh.base_length @@ -1846,12 +1857,19 @@ def length_scale_x(self, value: float): @property def length_scale_y(self): - """Constant multiplier of the base length scale on model gradients along y. + r"""Multiplier constant for smoothness along z relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_y` defines the multiplier constant for first-order smoothness along y, + the length-scale is given by: + + .. math:: + L_y = \bigg ( \frac{\alpha_y}{\Delta h} \bigg )^{1/2} Returns ------- float - Constant multiplier of the base length scale on model gradients along y. + Multiplier constant for smoothness along z relative to base scale length. """ return np.sqrt(self.alpha_y) / self.regularization_mesh.base_length @@ -1869,12 +1887,19 @@ def length_scale_y(self, value: float): @property def length_scale_z(self): - """Constant multiplier of the base length scale on model gradients along z. + r"""Multiplier constant for smoothness along z relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_z` defines the multiplier constant for first-order smoothness along z, + the length-scale is given by: + + .. math:: + L_z = \bigg ( \frac{\alpha_z}{\Delta h} \bigg )^{1/2} Returns ------- float - Constant multiplier of the base length scale on model gradients along z. + Multiplier constant for smoothness along z relative to base scale length. """ return np.sqrt(self.alpha_z) / self.regularization_mesh.base_length @@ -1892,12 +1917,12 @@ def length_scale_z(self, value: float): @property def reference_model_in_smooth(self) -> bool: - """Whether to include the reference model in the smoothness terms. + """Whether to include the reference model in the smoothness objective functions. Returns ------- bool - Whether to include the reference model in the smoothness terms. + Whether to include the reference model in the smoothness objective functions. """ return self._reference_model_in_smooth @@ -1956,12 +1981,21 @@ def _delta_m(self, m): @property def multipliers(self): - """Multipliers for weighted sum of objective functions. + r"""Multiplier constants for weighted sum of objective functions. + + For a model objective function :math:`\phi_m (\mathbf{m})` constructed using + a weighted sum of objective functions :math:`\phi_i (\mathbf{m})`, i.e.: + + .. math:: + \phi_m (\mathbf{m}) = \sum_i \alpha_i \, \phi_i (\mathbf{m}) + + the `multipliers` property returns the list of multiplier constants :math:`alpha_i` + in order. Returns ------- list of float - Multipliers for weighted sum of objective functions. + Multiplier constants for weighted sum of objective functions. """ return [getattr(self, objfct._multiplier_pair) for objfct in self.objfcts] @@ -1969,19 +2003,19 @@ def multipliers(self): def active_cells(self) -> np.ndarray: """Active cells defined on the regularization mesh. - A boolean array defining the cells in the :py:class:`~SimPEG.regularization.RegularizationMesh` - that are active throughout the inversion. Inactive cells remain fixed and are defined according - to the starting model. + A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` + that are active (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. Returns ------- - (n_cells, ) Array of bool + (n_cells, ) array of bool Notes ----- - If the property is set using an array of integers, the setter interprets the array as - representing the indices of the active cells. When called however, the quantity will have - been converted to a boolean array. + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. """ return self.regularization_mesh.active_cells @@ -2004,13 +2038,13 @@ def active_cells(self, values: np.ndarray): @property def reference_model(self) -> np.ndarray: - """Reference model values used to constrain the inversion. + """Reference model. Returns ------- - array_like - Reference model values used to constrain the inversion. If ``None``, - the reference model is equal to the starting model for the inversion. + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. """ return self._reference_model @@ -2144,7 +2178,7 @@ class BaseSimilarityMeasure(BaseRegularization): Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. mapping : SimPEG.maps.WireMap - Wire map connecting physical properties defined on the regularization mesh + Wire map connecting physical properties defined on active cells of the regularization mesh to the entire model. """ diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 3eba92c5f6..21788238f0 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -13,16 +13,19 @@ class RegularizationMesh(props.BaseSimPEG): - """ - **Regularization Mesh** - - This contains the operators used in the regularization. Note that these - are not necessarily true differential operators, but are constructed from - a `discretize` Mesh. - - :param discretize.base.BaseMesh mesh: problem mesh - :param numpy.ndarray active_cells: bool array, size nC, that is True where we have active cells. Used to reduce the operators so we regularize only on active cells - + """Regularization Mesh + + The ``RegularizationMesh`` class is used to construct discrete operators for + the objective function(s) defining the regularization. Discrete operators + only act on active cells in the inversion, thus reducing computational cost. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the discrete set of model parameters are defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of mesh cells that are active in the inversion. + If ``None``, all cells are active. """ regularization_type = None # or 'Base' @@ -35,12 +38,21 @@ def __init__(self, mesh, active_cells=None, **kwargs): @property def active_cells(self) -> np.ndarray: - """A boolean array indicating whether a cell is active + """Active cells on the regularization mesh. + + A boolean array defining the cells in the regularization mesh that are active + (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. + + Returns + ------- + (n_cells, ) array of bool Notes ----- - If this is set with an array of integers, it interprets it as an array - listing the active cell indices. + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. """ return self._active_cells @@ -80,8 +92,12 @@ def active_cells(self, values: np.ndarray): @property def vol(self) -> np.ndarray: - """ - Reduced volume vector. + """Volumes of active mesh cells. + + Returns + ------- + (n_active, ) numpy.ndarray of float + Volumes of active mesh cells. """ if self.active_cells is None: return self.mesh.cell_volumes @@ -91,8 +107,12 @@ def vol(self) -> np.ndarray: @property def nC(self) -> int: - """ - Number of cells being regularized. + """Number of active cells. + + Returns + ------- + int + Number of active cells. """ if self.active_cells is not None: return int(self.active_cells.sum()) @@ -100,16 +120,23 @@ def nC(self) -> int: @property def dim(self) -> int: - """ - Dimension of regularization mesh (1D, 2D, 3D) + """Dimension of regularization mesh. + + Returns + ------- + {1, 2, 3} + Dimension of the regularization mesh. """ return self.mesh.dim @property def Pac(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active cells to - full modelling space (ie. nC x nactive_cells). + """Projection matrix from active cells to all mesh cells. + + Returns + ------- + (n_cells, n_active) scipy.sparse.csr_matrix + Projection matrix from active cells to all mesh cells. """ if getattr(self, "_Pac", None) is None: if self.active_cells is None: @@ -120,9 +147,12 @@ def Pac(self) -> sp.csr_matrix: @property def Pafx(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active x-faces - to full modelling space (ie. nFx x nactive_cells_Fx ) + """Projection matrix from active x-faces to all x-faces in the mesh. + + Returns + ------- + (n_faces_x, n_active_x) scipy.sparse.csr_matrix + Projection matrix from active x-faces to all x-faces in the mesh """ if getattr(self, "_Pafx", None) is None: if self.mesh._meshType == "TREE": diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index c53863adf6..ae21f90bc4 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -33,12 +33,12 @@ class BaseSparse(BaseRegularization): mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the - regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray Reference model values used to constrain the inversion. If ``None``, the starting model is set as the reference model. @@ -69,8 +69,8 @@ def irls_scaled(self) -> bool: """Scale IRLS weights. When ``True``, scaling is applied when computing IRLS weights. - The scaling acts to preserve the balance between the data misfit and the model - objective function, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and objective functions + in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. For a comprehensive description, see the documentation for :py:meth:`get_lp_weights` . @@ -143,9 +143,9 @@ def get_lp_weights(self, f_m): least-squares regularization kernels. For :class:`SparseSmallness`, *f_m* is a (n_cells, ) ``numpy.ndarray``. - For :class:`SparseSmoothness`, *f_m* is a ``numpy.ndarray`` whose length corresponds to - the number of faces along a particular orientation; e.g. for smoothness along x, the length - is (n_faces_x, ). + For :class:`SparseSmoothness`, *f_m* is a ``numpy.ndarray`` whose length corresponds + to the number of faces along a particular orientation; e.g. for smoothness along x, + the length is (n_faces_x, ). Parameters ---------- @@ -167,8 +167,8 @@ def get_lp_weights(self, f_m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the model - objective function, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and objective functions + in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To apply elementwise scaling, let @@ -228,16 +228,15 @@ class SparseSmallness(BaseSparse, Smallness): The norm defining sparseness in the regularization function. Use a ``float`` to define the same norm for all mesh cells, or define an independent norm for each cell. All norm values must be within the interval [0, 2]. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`.regularization.RegularizationMesh` cells - that are active in the inversion. If ``None``, all cells are active. - mapping : None, .maps.IdentityMap - The mapping from the model parameters to the quantity defined in the regularization. - If ``None``, the mapping is the :class:`.maps.IdentityMap`. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting model - is set as the reference model by default. To include the reference model in the - regularization function, the `reference_model_in_smooth` property must be set to ``True``. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -267,7 +266,7 @@ class SparseSmallness(BaseSparse, Smallness): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function is given by: + function (objective function) is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i @@ -295,8 +294,8 @@ class SparseSmallness(BaseSparse, Smallness): \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex - least-squares problem for IRLS iteration :math:`k` can be expressed as follows: + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective + function for IRLS iteration :math:`k` can be expressed as follows: .. math:: \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, @@ -312,7 +311,7 @@ class SparseSmallness(BaseSparse, Smallness): Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights - for iteration :math:`k`. The net weighting applied within the discrete regularization function + for iteration :math:`k`. The net weighting applied within the objective function is given by: .. math:: @@ -373,8 +372,8 @@ def update_weights(self, m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the - regularization, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and objective functions + in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To compute the scaling, let @@ -386,15 +385,15 @@ def update_weights(self, m): .. math:: \tilde{f}_{\! i,max} = \begin{cases} - f_{max} \;\;\;\;\;\, for \; p_i \geq 1 \\ + f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 \end{cases} The scaling quantity :math:`\boldsymbol{\lambda}` is: .. math:: - \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] - \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2 \bigg ]^{1 - \mathbf{p}/2} + \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] + \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} """ f_m = self.f_m(m) self.set_weights(irls=self.get_lp_weights(f_m)) @@ -404,8 +403,8 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): r"""Sparse smoothness (blockiness) regularization. ``SparseSmoothness`` is used to recover models comprised of blocky structures. - The level of blockiness is controlled by the choice in norm within the regularization function; - with more blocky structures being recovered when a smaller norm is used. + The level of blockiness is controlled by the choice in norm within the regularization + function; with more blocky structures being recovered when a smaller norm is used. Optionally, custom cell weights can be included to control the degree of blockiness being enforced throughout different regions the model. @@ -426,15 +425,16 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, .maps.IdentityMap - The mapping from the model parameters to the quantity defined in the regularization. - If ``None``, the mapping is the :class:`.maps.IdentityMap`. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting - is set as the reference model by default. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -458,7 +458,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): Notes ----- - We define the regularization function for sparse smoothness (blockiness) along the x-direction + The regularization function for sparse smoothness (blockiness) along the x-direction as: .. math:: @@ -473,7 +473,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discrete approximation for the regularization - function is given by: + function (objective function) is given by: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \sum_i @@ -503,8 +503,8 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex - least-squares problem for IRLS iteration :math:`k` can be expressed as follows: + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective + function for IRLS iteration :math:`k` can be expressed as follows: .. math:: \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, @@ -522,8 +522,8 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): **Reference model in smoothness:** Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization function. - In this case, the discrete regularization function becomes: + preserved by including the reference model the smoothness regularization. + In this case, the objective function becomes: .. math:: \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} @@ -533,7 +533,8 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, - \mathbf{W}^{\! (k)} \mathbf{G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \mathbf{W}^{\! (k)} \mathbf{G_x} + \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter @@ -542,14 +543,14 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): **IRLS weights, user-defined weighting and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for - smoothness along the x-direction. Each set of weights were either defined directly on the - faces or have been averaged from cell centers. And let :math:`\mathbf{r_x}^{\!\! (k)}` represent - the IRLS weights for iteration :math:`k`. The net weighting applied within the discrete - regularization function is given by: + custom weights defined on the faces specified by the `orientation` property; + i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined + directly on the faces or have been averaged from cell centers. And let + :math:`\mathbf{r_x}^{\!\! (k)}` represent the IRLS weights for iteration :math:`k`. + The net weighting applied within the objective function is given by: .. math:: - \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \tilde{v} \odot \prod_i \mathbf{w_i} + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \tilde{v} \odot \prod_j \mathbf{w_j} where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes and dimensions when the regularization function is discretized to the mesh. @@ -607,8 +608,8 @@ def update_weights(self, m): where :math:`\mathbf{m}` is the model provided, :math:`\mathbf{G_x}` is the partial cell gradient operator along x (i.e. x-derivative), and :math:`\mathbf{m}^{(ref)}` is a reference model (optional, activated using `reference_model_in_smooth`). - See :py:meth:`Smoothness.f_m` for a more comprehensive definition of the least-squares - regularization kernel. + See :py:meth:`SmoothnessFirstOrder.f_m` for a more comprehensive definition of the + least-squares regularization kernel. However, when the class property `gradient_type`=`'total'`, IRLS weights are computed using the magnitude of the total gradient and we define: @@ -633,8 +634,8 @@ def update_weights(self, m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the model - objective function, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and objective functions + in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To apply the scaling, let @@ -646,15 +647,15 @@ def update_weights(self, m): .. math:: \tilde{f}_{\! i,max} = \begin{cases} - f_{max} \;\;\;\;\;\, for \; p_i \geq 1 \\ + f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 \end{cases} The scaling vector :math:`\boldsymbol{\lambda}` is: .. math:: - \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] - \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2 \bigg ]^{1 - \mathbf{p}/2} + \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] + \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} """ if self.gradient_type == "total": delta_m = self.mapping * self._delta_m(m) @@ -719,7 +720,7 @@ def gradient_type(self, value: str): class Sparse(WeightedLeastSquares): r"""Sparse norm weighted least squares regularization. - Construct a regularization for recovering compact and/or blocky structures + Apply regularization for recovering compact and/or blocky structures using a weighted sum of :class:`SparseSmallness` and :class:`SparseSmoothness` regularization functions. The level of compactness and blockiness is controlled by the norms within the respective regularization functions; @@ -734,15 +735,15 @@ class Sparse(WeightedLeastSquares): mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - active_cells : None, numpy.ndarray of bool - Array of bool defining the set of :py:class:`~.regularization.RegularizationMesh` + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined in the - regularization. If ``None``, the mapping is the identity map. + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the - reference model is equal to the starting model for the inversion. + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. reference_model_in_smooth : bool, optional Whether to include the reference model in the smoothness terms. units : None, str @@ -775,9 +776,10 @@ class Sparse(WeightedLeastSquares): Notes ----- - The model objective function :math:`\phi_m (m)` defined by a weighted sum of - :class:`SparseSmallness` and :class:`SparseSmoothness` regularization functions - is given by: + Sparse regularization can be defined by a weighted sum of + :class:`SparseSmallness` and :class:`SparseSmoothness` + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: .. math:: \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) @@ -786,14 +788,15 @@ class Sparse(WeightedLeastSquares): \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{p_j(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` - is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. - Constants :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` weight the respective - contributions of the smallness and smoothness regularization functions. - :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness throughout the recovered - model; where more compact structures are recovered in regions where :math:`p_s(r)` is small. - And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which impose sparse smoothness - throughout the recovered model along the specified direction; where sharper boundaries are - recovered in regions where these parameters are small. + is a user-defined weighting function applied to all terms. + :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. + Parameters :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` are multiplier constants + that weight the respective contributions of the smallness and smoothness terms in the + regularization. :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness + throughout the recovered model; where more compact structures are recovered in regions where + :math:`p_s(r)` is small. And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which + impose sparse smoothness throughout the recovered model along the specified direction; + where sharper boundaries are recovered in regions where these parameters are small. For implementation within SimPEG, regularization functions and their variables must be discretized onto a `mesh`. For a regularization function whose kernel is given by @@ -802,9 +805,13 @@ class Sparse(WeightedLeastSquares): .. math:: \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} - where :math:`f_i \in \mathbf{f_m}` define the discrete least-squares regularization kernel on the mesh; - e.g. for smallness regularization, the least-squares regularization kernel is - :math:`\mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)}`. + where :math:`f_i \in \mathbf{f_m}` define the discrete least-squares regularization kernel + on the mesh. For example, the least-squares regularization kernel for smallness regularization + is: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)} + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account for cell dimensions in the discretization and 2) apply user-defined weighting. :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). @@ -824,8 +831,9 @@ class Sparse(WeightedLeastSquares): r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the convex - least-squares problem for IRLS iteration :math:`k` can be expressed as follows: + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the model + objective function for IRLS iteration :math:`k` can be expressed as a weighted sum of + objective functions of the form: .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} @@ -845,17 +853,19 @@ class Sparse(WeightedLeastSquares): **IRLS weights, user-defined weighting and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell - weights that are applied to all regularization terms in the model objective function. + weights that are applied to all objective functions in the model objective function. For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness term is given by: .. math:: - \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{\tilde{v}_s} \odot \prod_j \mathbf{w_j} + \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot + \mathbf{\tilde{v}_s} \odot \prod_j \mathbf{w_j} And for sparse smoothness along x (likewise for y and z) is given by: .. math:: - \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{\tilde{v}_x} \odot \prod_j \mathbf{P_x \, w_j} + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{\tilde{v}_x} + \odot \prod_j \mathbf{P_x \, w_j} The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` for :math:`\ast = s,x,y,z`. The default weights that account for cell dimensions when the @@ -863,7 +873,7 @@ class Sparse(WeightedLeastSquares): for :math:`\ast = s,x,y,z`. Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` project the user-defined weighting to the appropriate faces. - Once the net weights for all terms in the model objective function are computed, + Once the net weights for all objective functions are computed, their weighting matrices can be constructed via: .. math:: @@ -882,8 +892,8 @@ class Sparse(WeightedLeastSquares): **Reference model in smoothness:** Gradients/interfaces within a discrete reference model can be preserved by including the - reference model the smoothness regularization functions. In this case, - the discrete regularization function becomes: + reference model the smoothness regularization. In this case, + the objective function becomes: .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} @@ -1087,7 +1097,7 @@ def update_weights(self, model): For an instance of the `Sparse` regularization class, this method re-computes and updates the IRLS for all child regularization objects using the model provided. To see how IRLS weights are recomputed for :class:`SparseSmallness` objects, visit the - documentation for :py:meth:`SparseSmallnes.update_weights`. And for + documentation for :py:meth:`SparseSmallness.update_weights`. And for :class:`SparseSmoothness` objects, visit the documentation for :py:meth:`SparseSmoothness.update_weights`. From 134e7ec52d1bb7421f070788d2d853007c1b1120 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 23 May 2023 15:37:33 -0700 Subject: [PATCH 082/455] add tests --- SimPEG/regularization/vector.py | 6 ++++- tests/base/test_regularization.py | 42 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 15f0c66c01..00246ff29c 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -98,5 +98,9 @@ def W(self): value.reshape((nC, mesh.dim), order="F"), axis=1 ) weights = np.sqrt(weights) - self._W = sp.diags(np.r_[weights, weights, weights], format="csr") + if mesh.dim == 2: + diag = weights + else: + diag = np.r_[weights, weights, weights] + self._W = sp.diags(diag, format="csr") return self._W diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index ce8b511114..4d48f22787 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -20,6 +20,7 @@ "BaseSimilarityMeasure", "SimpleComboRegularization", "BaseSparse", + "BaseVectorRegularization", "PGI", "PGIwithRelationships", "PGIwithNonlinearRelationshipsSmallness", @@ -27,6 +28,7 @@ "CrossGradient", "LinearCorrespondence", "JointTotalVariation", + "CrossReferenceRegularization", ] @@ -565,5 +567,45 @@ def test_WeightedLeastSquares(): np.testing.assert_allclose(reg.length_scale_z, 0.8) +@pytest.mark.parametrize("dim", [2, 3]) +def test_cross_ref_reg(dim): + mesh = discretize.TensorMesh([3, 4, 5][:dim]) + actives = mesh.cell_centers[:, -1] < 0.6 + n_active = actives.sum() + + ref_dir = dim * [1] + + cross_reg = regularization.CrossReferenceRegularization( + mesh, ref_dir, active_cells=actives + ) + + assert cross_reg.ref_dir.shape == (n_active, dim) + assert cross_reg._nC_residual == dim * n_active + + # give it some cell weights, and some cell vector weights to do something with + cell_weights = np.random.rand(n_active) + cell_vec_weights = np.random.rand(n_active, dim) + cross_reg.set_weights(cell_weights=cell_weights) + cross_reg.set_weights(vec_weights=cell_vec_weights) + + if dim == 3: + assert cross_reg.W.shape == (3 * n_active, 3 * n_active) + else: + assert cross_reg.W.shape == (n_active, n_active) + + m = np.random.rand(dim * n_active) + cross_reg.test(m) + + +def test_cross_reg_reg_errors(): + mesh = discretize.TensorMesh([3, 4, 5]) + + # bad ref_dir shape + ref_dir = np.random.rand(mesh.n_cells - 1, mesh.dim) + + with pytest.raises(ValueError, match="ref_dir"): + regularization.CrossReferenceRegularization(mesh, ref_dir) + + if __name__ == "__main__": unittest.main() From ce3b20f05f0c82d2da3273446c3ae1cd9d662562 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 23 May 2023 15:51:35 -0700 Subject: [PATCH 083/455] Add some documentation --- SimPEG/regularization/vector.py | 49 ++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 00246ff29c..f41cb71266 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -8,7 +8,11 @@ class BaseVectorRegularization(BaseRegularization): - """The regularizers work on models where each value is a vector.""" + """Base regularizer for models where each value is a vector. + + Used when your model has a multiple parameters for each cell. This can be helpful if + your model is made up of vector values in each cell or it is an anisotropic model. + """ @property def _weights_shapes(self) -> list[tuple[int]]: @@ -24,6 +28,43 @@ def _weights_shapes(self) -> list[tuple[int]]: class CrossReferenceRegularization(Smallness, BaseVectorRegularization): + """Vector regularization with a reference direction. + + This regularizer measures the magnitude of the cross product of the vector model + with a reference vector model. This encourages the vectors in the model to point + in the reference direction. The cross product of two vectors is minimized when they + are parallel (or anti-parallel) to each other, and maximized when the vectors are + perpendicular to each other. + + Parameters + ---------- + mesh : discretize.base.BaseMesh, .RegularizationMesh + The mesh defining the model discretization. + ref_dir : (mesh.dim,) array_like or (mesh.dim, n_active) array_like + The reference direction model. This can be either a constant vector applied + to every model cell, or different for every active model cell. + active_cells : index_array, optional + Boolean array or an array of active indices indicating the active cells of the + inversion domain mesh. + mapping : SimPEG.maps.IdentityMap, optional + An optional linear mapping that would go from the model space to the space where + the cross-product is enforced. + weights : dict of [str: array_like], optional + Any cell based weights for the regularization. Note if given a weight that is + (n_cells, dim), meaning it is dependent on the vector component, it will compute + the geometric mean of the component weights per cell and use that as a weight. + **kwargs + Arguments passed on to the parent classes: :py:class`.Smallness` and + :py:class`.BaseVectorRegularization`. + + Notes + ----- + The continuous form of this regularization looks like: + + .. math:: + \phi_{cross}(m) = \int_{V} ||\vec{m} \times \vec{m}_{ref}||^2 dV + """ + def __init__( self, mesh, ref_dir, active_cells=None, mapping=None, weights=None, **kwargs ): @@ -44,6 +85,12 @@ def _nC_residual(self): @property def ref_dir(self): + """The reference direction model + + Returns + ------- + (n_active, dim) numpy.ndarray + """ return self._ref_dir @ref_dir.setter From b7aaed51e98d4dbf55dd18c97f1c712e65f33563 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 23 May 2023 16:05:53 -0700 Subject: [PATCH 084/455] make r-string --- SimPEG/regularization/vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index f41cb71266..57ef3e3348 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -28,7 +28,7 @@ def _weights_shapes(self) -> list[tuple[int]]: class CrossReferenceRegularization(Smallness, BaseVectorRegularization): - """Vector regularization with a reference direction. + r"""Vector regularization with a reference direction. This regularizer measures the magnitude of the cross product of the vector model with a reference vector model. This encourages the vectors in the model to point From 6b97bd9b3ef076a68ef5854e7ce8d31175288da3 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 24 May 2023 09:48:38 -0700 Subject: [PATCH 085/455] fix for numpy 1.24 change --- SimPEG/flow/richards/survey.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/SimPEG/flow/richards/survey.py b/SimPEG/flow/richards/survey.py index da79f9bdab..cc6dd0ff47 100644 --- a/SimPEG/flow/richards/survey.py +++ b/SimPEG/flow/richards/survey.py @@ -3,6 +3,7 @@ from ...survey import BaseSurvey, BaseRx from ...utils import validate_list_of_types +from discretize.utils import Zero class Survey(BaseSurvey): @@ -80,12 +81,14 @@ def derivAdjoint(self, simulation, f, v=None): numpy.ndarray Adjoint derivative with respect to model times a vector """ - dd_du = list(range(len(self.receiver_list))) - dd_dm = list(range(len(self.receiver_list))) + dd_du = [] + dd_dm = [] cnt = 0 for ii, rx in enumerate(self.receiver_list): - dd_du[ii], dd_dm[ii] = rx.deriv( - f, simulation, v=v[cnt : cnt + rx.nD], adjoint=True - ) + du, dm = rx.deriv(f, simulation, v=v[cnt : cnt + rx.nD], adjoint=True) + if not isinstance(du, Zero): + dd_du.append(du) + if not isinstance(dm, Zero): + dd_dm.append(dm) cnt += rx.nD return np.sum(dd_du, axis=0), np.sum(dd_dm, axis=0) From a20ca4e6e85bb7b6d88d64d0938c2994e955823c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 24 May 2023 14:38:42 -0700 Subject: [PATCH 086/455] updates for flake --- SimPEG/flow/richards/survey.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/SimPEG/flow/richards/survey.py b/SimPEG/flow/richards/survey.py index cc6dd0ff47..fd7aad8be1 100644 --- a/SimPEG/flow/richards/survey.py +++ b/SimPEG/flow/richards/survey.py @@ -81,14 +81,12 @@ def derivAdjoint(self, simulation, f, v=None): numpy.ndarray Adjoint derivative with respect to model times a vector """ - dd_du = [] - dd_dm = [] + dd_du = 0 + dd_dm = 0 cnt = 0 - for ii, rx in enumerate(self.receiver_list): + for rx in self.receiver_list: du, dm = rx.deriv(f, simulation, v=v[cnt : cnt + rx.nD], adjoint=True) - if not isinstance(du, Zero): - dd_du.append(du) - if not isinstance(dm, Zero): - dd_dm.append(dm) + dd_du = dd_du + du + dd_dm = dd_dm + dm cnt += rx.nD - return np.sum(dd_du, axis=0), np.sum(dd_dm, axis=0) + return dd_du, dd_dm From aa1c373ccbd93f18f115bdc2f744a6eb3909ab3f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 24 May 2023 14:55:36 -0700 Subject: [PATCH 087/455] remove unused import --- SimPEG/flow/richards/survey.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/flow/richards/survey.py b/SimPEG/flow/richards/survey.py index fd7aad8be1..1f6848088d 100644 --- a/SimPEG/flow/richards/survey.py +++ b/SimPEG/flow/richards/survey.py @@ -3,7 +3,6 @@ from ...survey import BaseSurvey, BaseRx from ...utils import validate_list_of_types -from discretize.utils import Zero class Survey(BaseSurvey): From 885c5cffae771fa89b3f7bac72d7031fe4a576cb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 10:02:59 -0700 Subject: [PATCH 088/455] update component weight check --- SimPEG/regularization/vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 57ef3e3348..531633e0ca 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -140,7 +140,7 @@ def W(self): for value in self._weights.values(): if value.shape == (nC,): weights *= value - elif value.size == (mesh.dim * nC,): + elif value.size == mesh.dim * nC: weights *= np.linalg.norm( value.reshape((nC, mesh.dim), order="F"), axis=1 ) From 6c8cf49a0a415a0484b90c3a3a1b1837947565d6 Mon Sep 17 00:00:00 2001 From: dccowan Date: Thu, 25 May 2023 10:08:49 -0700 Subject: [PATCH 089/455] post SimPEG meeting changes --- SimPEG/regularization/base.py | 39 ++++++++++++------------------ SimPEG/regularization/sparse.py | 43 +++++++++++++++------------------ 2 files changed, 35 insertions(+), 47 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 5fc1569f70..7d5231f97a 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -559,10 +559,9 @@ class Smallness(BaseRegularization): custom cell weights. The weighting applied within the objective function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. + where :math:`\mathbf{v}` are the cell volumes. The weighting matrix used to apply the weights for smallness regularization is given by: .. math:: @@ -788,10 +787,9 @@ class SmoothnessFirstOrder(BaseRegularization): The weighting applied within the objective function is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} + \mathbf{\tilde{w}} = \mathbf{v_x} \odot \prod_j \mathbf{w_j} - where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized onto the mesh. + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces. The weighting matrix is given by: .. math:: @@ -1147,10 +1145,9 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. + where :math:`\mathbf{v}` are the cell volumes. The weighting matrix used to apply the weights is given by: .. math:: @@ -1444,13 +1441,12 @@ class WeightedLeastSquares(ComboObjectiveFunction): smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` and `alpha_zz` parameters are set to 0 by default. - The model objective function has been formulated such that each term is - roughly the same size when the :math:`\alpha` parameters are equal; e.g. when - :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying a - default weighting to each term which negates the effects of cell size/dimensions. - - Smoothness parameters can also be set using length scales. For example, by setting the - `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 @@ -1468,18 +1464,15 @@ class WeightedLeastSquares(ComboObjectiveFunction): is given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} and weights applied to first-order smoothness terms are given by: .. math:: - \mathbf{\tilde{w}} = \mathbf{\tilde{v}} \odot \prod_j \mathbf{P \, w_j} + \mathbf{\tilde{w}} = \big ( \mathbf{P \, v} \big ) \odot \prod_j \mathbf{P \, w_j} - :math:`\mathbf{\tilde{v}}` is a placeholder for the default weights that account for cell - volumes and dimensions when regularization functions are discretized onto the mesh. - These default weights vary depending on the regularization term; e.g. smallness, first-order - smoothness in the y-direction. For first-order smoothness terms, a projection matrix - :math:`\mathbf{P}` is required to average weights at cell centers to the appropriate faces; + :math:`\mathbf{v}` are the cell volumes, and :math:`\mathbf{P}` represents the + projection matrix from cell centers to the appropriate faces; i.e. where discrete first-order derivatives live. Weights for each term are used to construct their respective weighting matrices diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index ae21f90bc4..b155859635 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -315,10 +315,9 @@ class SparseSmallness(BaseSparse, Smallness): is given by: .. math:: - \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{\tilde{v}} \odot \prod_j \mathbf{w_j} + \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{v} \odot \prod_j \mathbf{w_j} - where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. + where :math:`\mathbf{v}` are the cell volumes. For a description of how IRLS weights are updated at every iteration, see the documentation for :py:meth:`update_weights`. @@ -550,12 +549,11 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): The net weighting applied within the objective function is given by: .. math:: - \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \tilde{v} \odot \prod_j \mathbf{w_j} + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{v_x} \odot \prod_j \mathbf{w_j} - where :math:`\mathbf{\tilde{v}}` are default weights that account for cell volumes - and dimensions when the regularization function is discretized to the mesh. - For a description of how IRLS weights are updated at every iteration, see the documentation - for :py:meth:`update_weights`. + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces; i.e. where the + x-derivative lives. For a description of how IRLS weights are updated at every iteration, + see the documentation for :py:meth:`update_weights`. The weighting matrix used to apply the weights is given by: @@ -859,19 +857,18 @@ class Sparse(WeightedLeastSquares): .. math:: \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot - \mathbf{\tilde{v}_s} \odot \prod_j \mathbf{w_j} + \mathbf{v} \odot \prod_j \mathbf{w_j} And for sparse smoothness along x (likewise for y and z) is given by: .. math:: - \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{\tilde{v}_x} + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \big ( \mathbf{P_x \, v} \big ) \odot \prod_j \mathbf{P_x \, w_j} The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` - for :math:`\ast = s,x,y,z`. The default weights that account for cell dimensions when the - regularization functions are discretized are defined as :math:`\mathbf{\tilde{v}_\ast}` - for :math:`\ast = s,x,y,z`. Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` - project the user-defined weighting to the appropriate faces. + for :math:`\ast = s,x,y,z`. :math:`\mathbf{v}` are the cell volumes. + Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` + project to the appropriate faces. Once the net weights for all objective functions are computed, their weighting matrices can be constructed via: @@ -908,18 +905,17 @@ class Sparse(WeightedLeastSquares): The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an - appropriate property of the ``Sparse`` class; e.g. :math:`\alpha_x` is set + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set using the `alpha_x` property. Note that unless the parameters are set manually, second-order smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` - and `alpha_zz` properties are set to 0 by default. + and `alpha_zz` parameters are set to 0 by default. - The model objective function has been formulated such that each term is - roughly the same size when the :math:`\alpha` parameters are equal; e.g. when - :math:`\alpha_s = \alpha_x = \alpha_y = \alpha_z` = ... This is accomplished by applying - a default weighting to each term which negates the effects of cell size/dimensions. - - Smoothness parameters can also be set using length scales. For example, by setting the - `length_scale_x` property, the `alpha_x` and `alpha_xx` properties are set as: + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 @@ -928,7 +924,6 @@ class Sparse(WeightedLeastSquares): >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 Likewise for y and z. - """ def __init__( From 294a905b3c69ec41407a1ca90f07a0a71ae5cc5c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 10:48:19 -0700 Subject: [PATCH 090/455] add correctness test --- SimPEG/meta/multiprocessing.py | 2 +- tests/meta/test_multiprocessing_sim.py | 371 +++++++++++++++++++++++++ 2 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 tests/meta/test_multiprocessing_sim.py diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 3b18fe8f1c..aac169435c 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -248,7 +248,7 @@ def Jtvec(self, m, v, f=None): jt_vec += p.result() return jt_vec - def getJtJdiag(self, m, v, f=None): + def getJtJdiag(self, m, W=None, f=None): self.model = m if getattr(self, "_jtjdiag", None) is None: if W is None: diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py new file mode 100644 index 0000000000..8863be4913 --- /dev/null +++ b/tests/meta/test_multiprocessing_sim.py @@ -0,0 +1,371 @@ +import numpy as np + +# from SimPEG.potential_fields import gravity +from SimPEG.electromagnetics.static import resistivity as dc +from SimPEG import maps +from discretize import TensorMesh +import scipy.sparse as sp +import pytest + +from SimPEG.meta import ( + MetaSimulation, + # SumMetaSimulation, + # RepeatedSimulation, + MultiprocessingMetaSimulation, +) + +mesh = TensorMesh([16, 16, 16], origin="CCN") + +rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] +rx_locs = rx_locs.reshape(3, -1).T +rxs = dc.receivers.Pole(rx_locs) +source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T +src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs +] + +m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + +# split by chunks of sources +chunk_size = 3 +sims = [] +mappings = [] +for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + sims.append( + dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) + ) + mappings.append(maps.IdentityMap()) + +serial_sim = MetaSimulation(sims, mappings) + + +@pytest.fixture +def parallel_sim(): + sim = MultiprocessingMetaSimulation(sims, mappings) + yield sim + sim.close() + + +def test_meta_correctness(parallel_sim): + # create fields objects + f_serial = serial_sim.fields(m_test) + f_parallel = parallel_sim.fields(m_test) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_serial) + d_mult = parallel_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult) + + # test Jvec + u = np.random.rand(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(serial_sim.survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult) + + # test things also works without passing optional fields + parallel_sim.model = m_test + d_mult2 = parallel_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2) + + jvec_mult2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2) + + jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + + # also pass a diagonal matrix here for testing. + parallel_sim._jtjdiag = None + W = sp.eye(parallel_sim.survey.nD) + diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) + np.testing.assert_allclose(diag_mult, diag_mult2) + + +# +# def test_sum_correctness(): +# mesh = TensorMesh([16, 16, 16], origin="CCN") +# +# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T +# rx = gravity.Point(rx_locs, components=["gz"]) +# survey = gravity.Survey(gravity.SourceField(rx)) +# serial_sim = gravity.Simulation3DIntegral( +# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +# ) +# +# mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) +# mesh_top = TensorMesh( +# [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] +# ) +# +# mappings = [ +# maps.Mesh2Mesh((mesh_bot, mesh)), +# maps.Mesh2Mesh((mesh_top, mesh)), +# ] +# sims = [ +# gravity.Simulation3DIntegral( +# mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +# ), +# gravity.Simulation3DIntegral( +# mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +# ), +# ] +# +# sum_sim = SumMetaSimulation(sims, mappings) +# +# m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 +# +# # test fields objects +# f_serial = serial_sim.fields(m_test) +# f_parallel = sum_sim.fields(m_test) +# np.testing.assert_allclose(f_serial, sum(f_parallel)) +# +# # test data output +# d_full = serial_sim.dpred(m_test, f=f_serial) +# d_mult = sum_sim.dpred(m_test, f=f_parallel) +# np.testing.assert_allclose(d_full, d_mult) +# +# # test Jvec +# u = np.random.rand(mesh.n_cells) +# jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) +# jvec_mult = sum_sim.Jvec(m_test, u, f=f_parallel) +# +# np.testing.assert_allclose(jvec_full, jvec_mult) +# +# # test Jtvec +# v = np.random.rand(survey.nD) +# jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) +# jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_parallel) +# +# np.testing.assert_allclose(jtvec_full, jtvec_mult) +# +# # test get diag +# diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) +# diag_mult = sum_sim.getJtJdiag(m_test, f=f_parallel) +# +# np.testing.assert_allclose(diag_full, diag_mult) +# +# # test things also works without passing optional kwargs +# sum_sim.model = m_test +# d_mult2 = sum_sim.dpred() +# np.testing.assert_allclose(d_mult, d_mult2) +# +# jvec_mult2 = sum_sim.Jvec(m_test, u) +# np.testing.assert_allclose(jvec_mult, jvec_mult2) +# +# jtvec_mult2 = sum_sim.Jtvec(m_test, v) +# np.testing.assert_allclose(jtvec_mult, jtvec_mult2) +# +# sum_sim._jtjdiag = None +# diag_mult2 = sum_sim.getJtJdiag(m_test) +# np.testing.assert_allclose(diag_mult, diag_mult2) +# +# +# def test_repeat_correctness(): +# # meta sim is tested for correctness +# # so can test the repeat against the meta sim +# mesh = TensorMesh([8, 8, 8], origin="CCN") +# +# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T +# rx = gravity.Point(rx_locs, components=["gz"]) +# survey = gravity.Survey(gravity.SourceField(rx)) +# sim = gravity.Simulation3DIntegral( +# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +# ) +# +# time_mesh = TensorMesh( +# [ +# 8, +# ], +# origin=[ +# 0, +# ], +# ) +# sim_ts = np.linspace(0, 1, 6) +# +# mappings = [] +# simulations = [] +# eye = sp.eye(mesh.n_cells, mesh.n_cells) +# for t in sim_ts: +# ave_time = time_mesh.get_interpolation_matrix( +# [ +# t, +# ] +# ) +# ave_full = sp.kron(ave_time, eye, format="csr") +# mappings.append(maps.LinearMap(ave_full)) +# simulations.append( +# gravity.Simulation3DIntegral( +# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +# ) +# ) +# +# parallel_sim = MetaSimulation(simulations, mappings) +# repeat_sim = RepeatedSimulation(sim, mappings) +# +# model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) +# +# # test field things +# f_serial = parallel_sim.fields(model) +# f_parallel = repeat_sim.fields(model) +# np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) +# +# d_full = parallel_sim.dpred(model, f_serial) +# d_repeat = repeat_sim.dpred(model, f_parallel) +# np.testing.assert_equal(d_full, d_repeat) +# +# # test Jvec +# u = np.random.rand(len(model)) +# jvec_full = parallel_sim.Jvec(model, u, f=f_serial) +# jvec_mult = repeat_sim.Jvec(model, u, f=f_parallel) +# np.testing.assert_allclose(jvec_full, jvec_mult) +# +# # test Jtvec +# v = np.random.rand(len(sim_ts) * survey.nD) +# jtvec_full = parallel_sim.Jtvec(model, v, f=f_serial) +# jtvec_mult = repeat_sim.Jtvec(model, v, f=f_parallel) +# np.testing.assert_allclose(jtvec_full, jtvec_mult) +# +# # test get diag +# diag_full = parallel_sim.getJtJdiag(model, f=f_serial) +# diag_mult = repeat_sim.getJtJdiag(model, f=f_parallel) +# np.testing.assert_allclose(diag_full, diag_mult) +# +# +# def test_meta_errors(): +# mesh = TensorMesh([16, 16, 16], origin="CCN") +# +# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] +# rx_locs = rx_locs.reshape(3, -1).T +# rxs = dc.receivers.Pole(rx_locs) +# source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T +# src_list = [ +# dc.sources.Pole( +# [ +# rxs, +# ], +# location=loc, +# ) +# for loc in source_locs +# ] +# +# # split by chunks of sources +# chunk_size = 3 +# sims = [] +# mappings = [] +# for i in range(0, len(src_list) + 1, chunk_size): +# end = min(i + chunk_size, len(src_list)) +# if i == end: +# break +# survey_chunk = dc.Survey(src_list[i:end]) +# sims.append( +# dc.Simulation3DNodal( +# mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap(mesh) +# ) +# ) +# mappings.append(maps.IdentityMap(mesh)) +# +# # incompatible length of mappings and simulations lists +# with pytest.raises(ValueError): +# MetaSimulation(sims[:-1], mappings) +# +# # mappings have incompatible input lengths: +# mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) +# with pytest.raises(ValueError): +# MetaSimulation(sims, mappings) +# +# # incompatible mapping and simulation +# mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) +# with pytest.raises(ValueError): +# MetaSimulation(sims, mappings) +# +# +# def test_sum_errors(): +# mesh = TensorMesh([16, 16, 16], origin="CCN") +# +# mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) +# mesh_top = TensorMesh( +# [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] +# ) +# +# mappings = [ +# maps.Mesh2Mesh((mesh_bot, mesh)), +# maps.Mesh2Mesh((mesh_top, mesh)), +# ] +# +# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T +# +# rx1 = gravity.Point(rx_locs, components=["gz"]) +# survey1 = gravity.Survey(gravity.SourceField(rx1)) +# rx2 = gravity.Point(rx_locs[1:], components=["gz"]) +# survey2 = gravity.Survey(gravity.SourceField(rx2)) +# +# sims = [ +# gravity.Simulation3DIntegral( +# mesh_bot, survey=survey1, rhoMap=maps.IdentityMap(mesh_bot), n_processes=1 +# ), +# gravity.Simulation3DIntegral( +# mesh_top, survey=survey2, rhoMap=maps.IdentityMap(mesh_top), n_processes=1 +# ), +# ] +# +# # Test simulations with different numbers of data. +# with pytest.raises(ValueError): +# SumMetaSimulation(sims, mappings) +# +# +# def test_repeat_errors(): +# mesh = TensorMesh([16, 16, 16], origin="CCN") +# +# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] +# rx_locs = rx_locs.reshape(3, -1).T +# rxs = dc.receivers.Pole(rx_locs) +# source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T +# src_list = [ +# dc.sources.Pole( +# [ +# rxs, +# ], +# location=loc, +# ) +# for loc in source_locs +# ] +# survey = dc.Survey(src_list) +# sim = dc.Simulation3DNodal(mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh)) +# +# # split by chunks of sources +# mappings = [] +# for _i in range(10): +# mappings.append(maps.IdentityMap(mesh)) +# +# # mappings have incompatible input lengths: +# mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) +# with pytest.raises(ValueError): +# RepeatedSimulation(sim, mappings) +# +# # incompatible mappings and simulations +# mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) +# with pytest.raises(ValueError): +# RepeatedSimulation(sim, mappings) From ab225ded2583e567030e7e15cd277ae71f110d6c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 11:27:10 -0700 Subject: [PATCH 091/455] add try to the close method and change order of close and join --- SimPEG/meta/multiprocessing.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index aac169435c..1d1e484175 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -268,10 +268,13 @@ def getJtJdiag(self, m, W=None, f=None): def close(self): for p in self._sim_processes: - if p.is_alive(): - p.task_queue.put(None) - p.join() - p.close() + try: + if p.is_alive(): + p.task_queue.put(None) + p.close() + p.join() + except ValueError: + pass def __del__(self): self.close() From e4403175cbcf145c0895248704331eef39a69563 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 11:27:35 -0700 Subject: [PATCH 092/455] add more multiprocessing sims (with tests) --- SimPEG/meta/__init__.py | 6 +- SimPEG/meta/multiprocessing.py | 91 ++++- tests/meta/test_multiprocessing_sim.py | 473 ++++++++++--------------- 3 files changed, 273 insertions(+), 297 deletions(-) diff --git a/SimPEG/meta/__init__.py b/SimPEG/meta/__init__.py index 276155ed4e..30aa3553b1 100644 --- a/SimPEG/meta/__init__.py +++ b/SimPEG/meta/__init__.py @@ -58,4 +58,8 @@ from .simulation import MetaSimulation, SumMetaSimulation, RepeatedSimulation -from .multiprocessing import MultiprocessingMetaSimulation +from .multiprocessing import ( + MultiprocessingMetaSimulation, + MultiprocessingSumMetaSimulation, + MultiprocessingRepeatedSimulation, +) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 1d1e484175..59f183585a 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -1,5 +1,5 @@ from multiprocessing import Process, Queue, cpu_count -from SimPEG.meta import MetaSimulation +from SimPEG.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation from SimPEG.props import HasModel import uuid import numpy as np @@ -45,7 +45,7 @@ def __init__(self, sim_chunk): def run(self): # everything here is local to the process # this sim is actually local to the running process and will - # persist between calls to field, dprec, jvec,... + # persist between calls to field, dpred, jvec,... sim = self.sim_chunk # a place to cache the field items locally _cached_items = {} @@ -278,3 +278,90 @@ def close(self): def __del__(self): self.close() + + +class MultiprocessingSumMetaSimulation( + MultiprocessingMetaSimulation, SumMetaSimulation +): + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_dpred(field) + + d_pred = 0 + for p in self._sim_processes: + d_pred += p.result() + return d_pred + + def Jvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_j_vec(v, field) + j_vec = 0 + for p in self._sim_processes: + j_vec += p.result() + return j_vec + + def Jtvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + p.start_jt_vec(v, field) + + jt_vec = 0 + for p in self._sim_processes: + jt_vec += p.result() + return jt_vec + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + p.start_jtj_diag(W, field) + jtj_diag = 0.0 + for p in self._sim_processes: + jtj_diag += p.result() + self._jtjdiag = jtj_diag + return self._jtjdiag + + +class MultiprocessingRepeatedSimulation( + MultiprocessingMetaSimulation, RepeatedSimulation +): + def __init__(self, simulation, mappings, n_processes=None): + # do this to call the initializer of the Repeated Sim + super(MultiprocessingMetaSimulation, self).__init__(simulation, mappings) + + if n_processes is None: + n_processes = cpu_count() + + # split mappings up into chunks + # (Which are currently defined using MetaSimulations) + n_sim = len(mappings) + chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] + for i in range(n_sim % n_processes): + chunk_sizes[i] += 1 + + processes = [] + i_start = 0 + for chunk in chunk_sizes: + if chunk == 0: + continue + i_end = i_start + chunk + sim_chunk = RepeatedSimulation( + self.simulation, self.mappings[i_start:i_end] + ) + p = _SimulationProcess(sim_chunk) + processes.append(p) + p.start() + i_start = i_end + + self._sim_processes = processes diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 8863be4913..dc6546028a 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -1,6 +1,6 @@ import numpy as np -# from SimPEG.potential_fields import gravity +from SimPEG.potential_fields import gravity from SimPEG.electromagnetics.static import resistivity as dc from SimPEG import maps from discretize import TensorMesh @@ -9,9 +9,11 @@ from SimPEG.meta import ( MetaSimulation, - # SumMetaSimulation, - # RepeatedSimulation, + SumMetaSimulation, + RepeatedSimulation, MultiprocessingMetaSimulation, + MultiprocessingSumMetaSimulation, + MultiprocessingRepeatedSimulation, ) mesh = TensorMesh([16, 16, 16], origin="CCN") @@ -34,338 +36,221 @@ # split by chunks of sources chunk_size = 3 -sims = [] -mappings = [] +dc_sims = [] +dc_mappings = [] for i in range(0, len(src_list) + 1, chunk_size): end = min(i + chunk_size, len(src_list)) if i == end: break survey_chunk = dc.Survey(src_list[i:end]) - sims.append( + dc_sims.append( dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) ) - mappings.append(maps.IdentityMap()) + dc_mappings.append(maps.IdentityMap()) -serial_sim = MetaSimulation(sims, mappings) +serial_dc_sim = MetaSimulation(dc_sims, dc_mappings) @pytest.fixture -def parallel_sim(): - sim = MultiprocessingMetaSimulation(sims, mappings) +def parallel_dc_sim(): + sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) yield sim sim.close() -def test_meta_correctness(parallel_sim): +def test_meta_correctness(parallel_dc_sim): # create fields objects - f_serial = serial_sim.fields(m_test) - f_parallel = parallel_sim.fields(m_test) + f_serial = serial_dc_sim.fields(m_test) + f_parallel = parallel_dc_sim.fields(m_test) # test data output - d_full = serial_sim.dpred(m_test, f=f_serial) - d_mult = parallel_sim.dpred(m_test, f=f_parallel) + d_full = serial_dc_sim.dpred(m_test, f=f_serial) + d_mult = parallel_dc_sim.dpred(m_test, f=f_parallel) np.testing.assert_allclose(d_full, d_mult) # test Jvec u = np.random.rand(mesh.n_cells) - jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) - jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + jvec_full = serial_dc_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_dc_sim.Jvec(m_test, u, f=f_parallel) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(serial_sim.survey.nD) - jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) - jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + v = np.random.rand(serial_dc_sim.survey.nD) + jtvec_full = serial_dc_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_dc_sim.Jtvec(m_test, v, f=f_parallel) np.testing.assert_allclose(jtvec_full, jtvec_mult) # test get diag - diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) - diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + diag_full = serial_dc_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_dc_sim.getJtJdiag(m_test, f=f_parallel) np.testing.assert_allclose(diag_full, diag_mult) # test things also works without passing optional fields - parallel_sim.model = m_test - d_mult2 = parallel_sim.dpred() + parallel_dc_sim.model = m_test + d_mult2 = parallel_dc_sim.dpred() np.testing.assert_allclose(d_mult, d_mult2) - jvec_mult2 = parallel_sim.Jvec(m_test, u) + jvec_mult2 = parallel_dc_sim.Jvec(m_test, u) np.testing.assert_allclose(jvec_mult, jvec_mult2) - jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + jtvec_mult2 = parallel_dc_sim.Jtvec(m_test, v) np.testing.assert_allclose(jtvec_mult, jtvec_mult2) # also pass a diagonal matrix here for testing. - parallel_sim._jtjdiag = None - W = sp.eye(parallel_sim.survey.nD) - diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) + parallel_dc_sim._jtjdiag = None + W = sp.eye(parallel_dc_sim.survey.nD) + diag_mult2 = parallel_dc_sim.getJtJdiag(m_test, W=W) np.testing.assert_allclose(diag_mult, diag_mult2) -# -# def test_sum_correctness(): -# mesh = TensorMesh([16, 16, 16], origin="CCN") -# -# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T -# rx = gravity.Point(rx_locs, components=["gz"]) -# survey = gravity.Survey(gravity.SourceField(rx)) -# serial_sim = gravity.Simulation3DIntegral( -# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -# ) -# -# mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) -# mesh_top = TensorMesh( -# [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] -# ) -# -# mappings = [ -# maps.Mesh2Mesh((mesh_bot, mesh)), -# maps.Mesh2Mesh((mesh_top, mesh)), -# ] -# sims = [ -# gravity.Simulation3DIntegral( -# mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -# ), -# gravity.Simulation3DIntegral( -# mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -# ), -# ] -# -# sum_sim = SumMetaSimulation(sims, mappings) -# -# m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 -# -# # test fields objects -# f_serial = serial_sim.fields(m_test) -# f_parallel = sum_sim.fields(m_test) -# np.testing.assert_allclose(f_serial, sum(f_parallel)) -# -# # test data output -# d_full = serial_sim.dpred(m_test, f=f_serial) -# d_mult = sum_sim.dpred(m_test, f=f_parallel) -# np.testing.assert_allclose(d_full, d_mult) -# -# # test Jvec -# u = np.random.rand(mesh.n_cells) -# jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) -# jvec_mult = sum_sim.Jvec(m_test, u, f=f_parallel) -# -# np.testing.assert_allclose(jvec_full, jvec_mult) -# -# # test Jtvec -# v = np.random.rand(survey.nD) -# jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) -# jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_parallel) -# -# np.testing.assert_allclose(jtvec_full, jtvec_mult) -# -# # test get diag -# diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) -# diag_mult = sum_sim.getJtJdiag(m_test, f=f_parallel) -# -# np.testing.assert_allclose(diag_full, diag_mult) -# -# # test things also works without passing optional kwargs -# sum_sim.model = m_test -# d_mult2 = sum_sim.dpred() -# np.testing.assert_allclose(d_mult, d_mult2) -# -# jvec_mult2 = sum_sim.Jvec(m_test, u) -# np.testing.assert_allclose(jvec_mult, jvec_mult2) -# -# jtvec_mult2 = sum_sim.Jtvec(m_test, v) -# np.testing.assert_allclose(jtvec_mult, jtvec_mult2) -# -# sum_sim._jtjdiag = None -# diag_mult2 = sum_sim.getJtJdiag(m_test) -# np.testing.assert_allclose(diag_mult, diag_mult2) -# -# -# def test_repeat_correctness(): -# # meta sim is tested for correctness -# # so can test the repeat against the meta sim -# mesh = TensorMesh([8, 8, 8], origin="CCN") -# -# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T -# rx = gravity.Point(rx_locs, components=["gz"]) -# survey = gravity.Survey(gravity.SourceField(rx)) -# sim = gravity.Simulation3DIntegral( -# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -# ) -# -# time_mesh = TensorMesh( -# [ -# 8, -# ], -# origin=[ -# 0, -# ], -# ) -# sim_ts = np.linspace(0, 1, 6) -# -# mappings = [] -# simulations = [] -# eye = sp.eye(mesh.n_cells, mesh.n_cells) -# for t in sim_ts: -# ave_time = time_mesh.get_interpolation_matrix( -# [ -# t, -# ] -# ) -# ave_full = sp.kron(ave_time, eye, format="csr") -# mappings.append(maps.LinearMap(ave_full)) -# simulations.append( -# gravity.Simulation3DIntegral( -# mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -# ) -# ) -# -# parallel_sim = MetaSimulation(simulations, mappings) -# repeat_sim = RepeatedSimulation(sim, mappings) -# -# model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) -# -# # test field things -# f_serial = parallel_sim.fields(model) -# f_parallel = repeat_sim.fields(model) -# np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) -# -# d_full = parallel_sim.dpred(model, f_serial) -# d_repeat = repeat_sim.dpred(model, f_parallel) -# np.testing.assert_equal(d_full, d_repeat) -# -# # test Jvec -# u = np.random.rand(len(model)) -# jvec_full = parallel_sim.Jvec(model, u, f=f_serial) -# jvec_mult = repeat_sim.Jvec(model, u, f=f_parallel) -# np.testing.assert_allclose(jvec_full, jvec_mult) -# -# # test Jtvec -# v = np.random.rand(len(sim_ts) * survey.nD) -# jtvec_full = parallel_sim.Jtvec(model, v, f=f_serial) -# jtvec_mult = repeat_sim.Jtvec(model, v, f=f_parallel) -# np.testing.assert_allclose(jtvec_full, jtvec_mult) -# -# # test get diag -# diag_full = parallel_sim.getJtJdiag(model, f=f_serial) -# diag_mult = repeat_sim.getJtJdiag(model, f=f_parallel) -# np.testing.assert_allclose(diag_full, diag_mult) -# -# -# def test_meta_errors(): -# mesh = TensorMesh([16, 16, 16], origin="CCN") -# -# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] -# rx_locs = rx_locs.reshape(3, -1).T -# rxs = dc.receivers.Pole(rx_locs) -# source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T -# src_list = [ -# dc.sources.Pole( -# [ -# rxs, -# ], -# location=loc, -# ) -# for loc in source_locs -# ] -# -# # split by chunks of sources -# chunk_size = 3 -# sims = [] -# mappings = [] -# for i in range(0, len(src_list) + 1, chunk_size): -# end = min(i + chunk_size, len(src_list)) -# if i == end: -# break -# survey_chunk = dc.Survey(src_list[i:end]) -# sims.append( -# dc.Simulation3DNodal( -# mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap(mesh) -# ) -# ) -# mappings.append(maps.IdentityMap(mesh)) -# -# # incompatible length of mappings and simulations lists -# with pytest.raises(ValueError): -# MetaSimulation(sims[:-1], mappings) -# -# # mappings have incompatible input lengths: -# mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) -# with pytest.raises(ValueError): -# MetaSimulation(sims, mappings) -# -# # incompatible mapping and simulation -# mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) -# with pytest.raises(ValueError): -# MetaSimulation(sims, mappings) -# -# -# def test_sum_errors(): -# mesh = TensorMesh([16, 16, 16], origin="CCN") -# -# mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) -# mesh_top = TensorMesh( -# [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] -# ) -# -# mappings = [ -# maps.Mesh2Mesh((mesh_bot, mesh)), -# maps.Mesh2Mesh((mesh_top, mesh)), -# ] -# -# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T -# -# rx1 = gravity.Point(rx_locs, components=["gz"]) -# survey1 = gravity.Survey(gravity.SourceField(rx1)) -# rx2 = gravity.Point(rx_locs[1:], components=["gz"]) -# survey2 = gravity.Survey(gravity.SourceField(rx2)) -# -# sims = [ -# gravity.Simulation3DIntegral( -# mesh_bot, survey=survey1, rhoMap=maps.IdentityMap(mesh_bot), n_processes=1 -# ), -# gravity.Simulation3DIntegral( -# mesh_top, survey=survey2, rhoMap=maps.IdentityMap(mesh_top), n_processes=1 -# ), -# ] -# -# # Test simulations with different numbers of data. -# with pytest.raises(ValueError): -# SumMetaSimulation(sims, mappings) -# -# -# def test_repeat_errors(): -# mesh = TensorMesh([16, 16, 16], origin="CCN") -# -# rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] -# rx_locs = rx_locs.reshape(3, -1).T -# rxs = dc.receivers.Pole(rx_locs) -# source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T -# src_list = [ -# dc.sources.Pole( -# [ -# rxs, -# ], -# location=loc, -# ) -# for loc in source_locs -# ] -# survey = dc.Survey(src_list) -# sim = dc.Simulation3DNodal(mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh)) -# -# # split by chunks of sources -# mappings = [] -# for _i in range(10): -# mappings.append(maps.IdentityMap(mesh)) -# -# # mappings have incompatible input lengths: -# mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) -# with pytest.raises(ValueError): -# RepeatedSimulation(sim, mappings) -# -# # incompatible mappings and simulations -# mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) -# with pytest.raises(ValueError): -# RepeatedSimulation(sim, mappings) +# Create gravity sum sims +rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T +rx = gravity.Point(rx_locs, components=["gz"]) +survey = gravity.Survey(gravity.SourceField(rx)) + +mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) +mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] +) + +g_mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), +] +g_sims = [ + gravity.Simulation3DIntegral( + mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + gravity.Simulation3DIntegral( + mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), +] + +serial_grav_sim = SumMetaSimulation(g_sims, g_mappings) + + +@pytest.fixture +def parallel_grav_sim(): + sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings) + yield sim + sim.close() + + +def test_sum_correctness(parallel_grav_sim): + # test fields objects + f_serial = serial_grav_sim.fields(m_test) + f_parallel = parallel_grav_sim.fields(m_test) + # np.testing.assert_allclose(f_serial, sum(f_parallel)) + + # test data output + d_full = serial_grav_sim.dpred(m_test, f=f_serial) + d_mult = parallel_grav_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult) + + # test Jvec + u = np.random.rand(mesh.n_cells) + jvec_full = serial_grav_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_grav_sim.Jvec(m_test, u, f=f_parallel) + + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(survey.nD) + jtvec_full = serial_grav_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_grav_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_grav_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_grav_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult) + + # test things also works without passing optional kwargs + parallel_grav_sim.model = m_test + d_mult2 = parallel_grav_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2) + + jvec_mult2 = parallel_grav_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2) + + jtvec_mult2 = parallel_grav_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + + parallel_grav_sim._jtjdiag = None + diag_mult2 = parallel_grav_sim.getJtJdiag(m_test) + np.testing.assert_allclose(diag_mult, diag_mult2) + + +############ +# Repeat Sim +rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T +rx = gravity.Point(rx_locs, components=["gz"]) +survey = gravity.Survey(gravity.SourceField(rx)) +grav_sim = gravity.Simulation3DIntegral( + mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 +) + +time_mesh = TensorMesh( + [ + 8, + ], + origin=[ + 0, + ], +) +sim_ts = np.linspace(0, 1, 6) + +repeat_mappings = [] +repeat_simulations = [] +eye = sp.eye(mesh.n_cells, mesh.n_cells) +for t in sim_ts: + ave_time = time_mesh.get_interpolation_matrix( + [ + t, + ] + ) + ave_full = sp.kron(ave_time, eye, format="csr") + repeat_mappings.append(maps.LinearMap(ave_full)) + +r_serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) +t_model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) + + +@pytest.fixture +def r_parallel_sim(): + sim = MultiprocessingRepeatedSimulation(grav_sim, repeat_mappings) + yield sim + sim.close() + + +def test_repeat_correctness(r_parallel_sim): + # test field things + f_serial = r_serial_sim.fields(t_model) + f_parallel = r_parallel_sim.fields(t_model) + # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) + + d_full = r_serial_sim.dpred(t_model, f_serial) + d_repeat = r_parallel_sim.dpred(t_model, f_parallel) + np.testing.assert_equal(d_full, d_repeat) + + # test Jvec + u = np.random.rand(len(t_model)) + jvec_full = r_serial_sim.Jvec(t_model, u, f=f_serial) + jvec_mult = r_parallel_sim.Jvec(t_model, u, f=f_parallel) + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(len(sim_ts) * survey.nD) + jtvec_full = r_serial_sim.Jtvec(t_model, v, f=f_serial) + jtvec_mult = r_parallel_sim.Jtvec(t_model, v, f=f_parallel) + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = r_serial_sim.getJtJdiag(t_model, f=f_serial) + diag_mult = r_parallel_sim.getJtJdiag(t_model, f=f_parallel) + np.testing.assert_allclose(diag_full, diag_mult) From c7f5a031fb1e414e8cdb451d4f985b02379f6d24 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 11:42:00 -0700 Subject: [PATCH 093/455] fix unused loop variables --- SimPEG/meta/multiprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 59f183585a..2d93a52662 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -311,7 +311,7 @@ def Jtvec(self, m, v, f=None): self.model = m if f is None: f = self.fields(m) - for i, (p, field) in enumerate(zip(self._sim_processes, f)): + for p, field in zip(self._sim_processes, f): p.start_jt_vec(v, field) jt_vec = 0 @@ -324,7 +324,7 @@ def getJtJdiag(self, m, W=None, f=None): if getattr(self, "_jtjdiag", None) is None: if f is None: f = self.fields(m) - for i, (p, field) in enumerate(zip(self._sim_processes, f)): + for p, field in zip(self._sim_processes, f): p.start_jtj_diag(W, field) jtj_diag = 0.0 for p in self._sim_processes: From f5feba1c36482569d6ac777c4cd1cd8fd16ce3eb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 19:13:41 -0700 Subject: [PATCH 094/455] more switches --- SimPEG/meta/multiprocessing.py | 19 +- tests/meta/test_multiprocessing_sim.py | 463 ++++++++++++------------- 2 files changed, 239 insertions(+), 243 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 2d93a52662..2b1d66c0b5 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -161,6 +161,11 @@ class MultiprocessingMetaSimulation(MetaSimulation): ... sim = MultiprocessingMetaSimulation(...) ... sim.dpred(model) + You must also be sure to call sim.close() before discarding + this worker to kill the subprocesses that are created, as you would with + any other multiprocessing queue. + + >>> sim.close() """ def __init__(self, simulations, mappings, n_processes=None): @@ -268,16 +273,10 @@ def getJtJdiag(self, m, W=None, f=None): def close(self): for p in self._sim_processes: - try: - if p.is_alive(): - p.task_queue.put(None) - p.close() - p.join() - except ValueError: - pass - - def __del__(self): - self.close() + if p.is_alive(): + p.task_queue.put(None) + p.join() + p.close() class MultiprocessingSumMetaSimulation( diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index dc6546028a..fbd4bd6797 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -16,241 +16,238 @@ MultiprocessingRepeatedSimulation, ) -mesh = TensorMesh([16, 16, 16], origin="CCN") - -rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] -rx_locs = rx_locs.reshape(3, -1).T -rxs = dc.receivers.Pole(rx_locs) -source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T -src_list = [ - dc.sources.Pole( - [ - rxs, - ], - location=loc, - ) - for loc in source_locs -] - -m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 - -# split by chunks of sources -chunk_size = 3 -dc_sims = [] -dc_mappings = [] -for i in range(0, len(src_list) + 1, chunk_size): - end = min(i + chunk_size, len(src_list)) - if i == end: - break - survey_chunk = dc.Survey(src_list[i:end]) - dc_sims.append( - dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) - ) - dc_mappings.append(maps.IdentityMap()) - -serial_dc_sim = MetaSimulation(dc_sims, dc_mappings) - - -@pytest.fixture -def parallel_dc_sim(): - sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) - yield sim - sim.close() - - -def test_meta_correctness(parallel_dc_sim): - # create fields objects - f_serial = serial_dc_sim.fields(m_test) - f_parallel = parallel_dc_sim.fields(m_test) - - # test data output - d_full = serial_dc_sim.dpred(m_test, f=f_serial) - d_mult = parallel_dc_sim.dpred(m_test, f=f_parallel) - np.testing.assert_allclose(d_full, d_mult) - - # test Jvec - u = np.random.rand(mesh.n_cells) - jvec_full = serial_dc_sim.Jvec(m_test, u, f=f_serial) - jvec_mult = parallel_dc_sim.Jvec(m_test, u, f=f_parallel) - - np.testing.assert_allclose(jvec_full, jvec_mult) - - # test Jtvec - v = np.random.rand(serial_dc_sim.survey.nD) - jtvec_full = serial_dc_sim.Jtvec(m_test, v, f=f_serial) - jtvec_mult = parallel_dc_sim.Jtvec(m_test, v, f=f_parallel) - - np.testing.assert_allclose(jtvec_full, jtvec_mult) - - # test get diag - diag_full = serial_dc_sim.getJtJdiag(m_test, f=f_serial) - diag_mult = parallel_dc_sim.getJtJdiag(m_test, f=f_parallel) - - np.testing.assert_allclose(diag_full, diag_mult) - - # test things also works without passing optional fields - parallel_dc_sim.model = m_test - d_mult2 = parallel_dc_sim.dpred() - np.testing.assert_allclose(d_mult, d_mult2) - - jvec_mult2 = parallel_dc_sim.Jvec(m_test, u) - np.testing.assert_allclose(jvec_mult, jvec_mult2) - - jtvec_mult2 = parallel_dc_sim.Jtvec(m_test, v) - np.testing.assert_allclose(jtvec_mult, jtvec_mult2) - - # also pass a diagonal matrix here for testing. - parallel_dc_sim._jtjdiag = None - W = sp.eye(parallel_dc_sim.survey.nD) - diag_mult2 = parallel_dc_sim.getJtJdiag(m_test, W=W) - np.testing.assert_allclose(diag_mult, diag_mult2) - - -# Create gravity sum sims -rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T -rx = gravity.Point(rx_locs, components=["gz"]) -survey = gravity.Survey(gravity.SourceField(rx)) - -mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) -mesh_top = TensorMesh( - [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] -) -g_mappings = [ - maps.Mesh2Mesh((mesh_bot, mesh)), - maps.Mesh2Mesh((mesh_top, mesh)), -] -g_sims = [ - gravity.Simulation3DIntegral( - mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 - ), - gravity.Simulation3DIntegral( - mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 - ), -] - -serial_grav_sim = SumMetaSimulation(g_sims, g_mappings) - - -@pytest.fixture -def parallel_grav_sim(): - sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings) - yield sim - sim.close() - - -def test_sum_correctness(parallel_grav_sim): - # test fields objects - f_serial = serial_grav_sim.fields(m_test) - f_parallel = parallel_grav_sim.fields(m_test) - # np.testing.assert_allclose(f_serial, sum(f_parallel)) - - # test data output - d_full = serial_grav_sim.dpred(m_test, f=f_serial) - d_mult = parallel_grav_sim.dpred(m_test, f=f_parallel) - np.testing.assert_allclose(d_full, d_mult) - - # test Jvec - u = np.random.rand(mesh.n_cells) - jvec_full = serial_grav_sim.Jvec(m_test, u, f=f_serial) - jvec_mult = parallel_grav_sim.Jvec(m_test, u, f=f_parallel) - - np.testing.assert_allclose(jvec_full, jvec_mult) - - # test Jtvec - v = np.random.rand(survey.nD) - jtvec_full = serial_grav_sim.Jtvec(m_test, v, f=f_serial) - jtvec_mult = parallel_grav_sim.Jtvec(m_test, v, f=f_parallel) - - np.testing.assert_allclose(jtvec_full, jtvec_mult) - - # test get diag - diag_full = serial_grav_sim.getJtJdiag(m_test, f=f_serial) - diag_mult = parallel_grav_sim.getJtJdiag(m_test, f=f_parallel) - - np.testing.assert_allclose(diag_full, diag_mult) - - # test things also works without passing optional kwargs - parallel_grav_sim.model = m_test - d_mult2 = parallel_grav_sim.dpred() - np.testing.assert_allclose(d_mult, d_mult2) - - jvec_mult2 = parallel_grav_sim.Jvec(m_test, u) - np.testing.assert_allclose(jvec_mult, jvec_mult2) - - jtvec_mult2 = parallel_grav_sim.Jtvec(m_test, v) - np.testing.assert_allclose(jtvec_mult, jtvec_mult2) - - parallel_grav_sim._jtjdiag = None - diag_mult2 = parallel_grav_sim.getJtJdiag(m_test) - np.testing.assert_allclose(diag_mult, diag_mult2) - - -############ -# Repeat Sim -rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T -rx = gravity.Point(rx_locs, components=["gz"]) -survey = gravity.Survey(gravity.SourceField(rx)) -grav_sim = gravity.Simulation3DIntegral( - mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 -) +def test_meta_correctness(): + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + # split by chunks of sources + chunk_size = 3 + dc_sims = [] + dc_mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + dc_sims.append( + dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) + ) + dc_mappings.append(maps.IdentityMap()) + + serial_sim = MetaSimulation(dc_sims, dc_mappings) + parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) + + try: + # create fields objects + f_serial = serial_sim.fields(m_test) + f_parallel = parallel_sim.fields(m_test) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_serial) + d_mult = parallel_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult) + + # test Jvec + u = np.random.rand(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(serial_sim.survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult) + + # test things also works without passing optional fields + parallel_sim.model = m_test + d_mult2 = parallel_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2) + + jvec_mult2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2) + + jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + + # also pass a diagonal matrix here for testing. + parallel_sim._jtjdiag = None + W = sp.eye(parallel_sim.survey.nD) + diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) + np.testing.assert_allclose(diag_mult, diag_mult2) + except Exception as err: + raise err + finally: + parallel_sim.close() + + +def test_sum_correctness(): + mesh = TensorMesh([16, 16, 16], origin="CCN") + # Create gravity sum sims + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) -time_mesh = TensorMesh( - [ - 8, - ], - origin=[ - 0, - ], -) -sim_ts = np.linspace(0, 1, 6) + g_mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + g_sims = [ + gravity.Simulation3DIntegral( + mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + gravity.Simulation3DIntegral( + mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + ] + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + serial_sim = SumMetaSimulation(g_sims, g_mappings) + parallel_sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings) + try: + # test fields objects + f_serial = serial_sim.fields(m_test) + f_parallel = parallel_sim.fields(m_test) + # np.testing.assert_allclose(f_serial, sum(f_parallel)) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_serial) + d_mult = parallel_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult) + + # test Jvec + u = np.random.rand(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult) + + # test things also works without passing optional kwargs + parallel_sim.model = m_test + d_mult2 = parallel_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2) + + jvec_mult2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2) + + jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + + parallel_sim._jtjdiag = None + diag_mult2 = parallel_sim.getJtJdiag(m_test) + np.testing.assert_allclose(diag_mult, diag_mult2) + + except Exception as err: + raise err + finally: + parallel_sim.close() + + +def test_repeat_correctness(): + mesh = TensorMesh([16, 16, 16], origin="CCN") + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + grav_sim = gravity.Simulation3DIntegral( + mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ) -repeat_mappings = [] -repeat_simulations = [] -eye = sp.eye(mesh.n_cells, mesh.n_cells) -for t in sim_ts: - ave_time = time_mesh.get_interpolation_matrix( + time_mesh = TensorMesh( [ - t, - ] + 8, + ], + origin=[ + 0, + ], ) - ave_full = sp.kron(ave_time, eye, format="csr") - repeat_mappings.append(maps.LinearMap(ave_full)) - -r_serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) -t_model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) - - -@pytest.fixture -def r_parallel_sim(): - sim = MultiprocessingRepeatedSimulation(grav_sim, repeat_mappings) - yield sim - sim.close() - - -def test_repeat_correctness(r_parallel_sim): - # test field things - f_serial = r_serial_sim.fields(t_model) - f_parallel = r_parallel_sim.fields(t_model) - # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) - - d_full = r_serial_sim.dpred(t_model, f_serial) - d_repeat = r_parallel_sim.dpred(t_model, f_parallel) - np.testing.assert_equal(d_full, d_repeat) - - # test Jvec - u = np.random.rand(len(t_model)) - jvec_full = r_serial_sim.Jvec(t_model, u, f=f_serial) - jvec_mult = r_parallel_sim.Jvec(t_model, u, f=f_parallel) - np.testing.assert_allclose(jvec_full, jvec_mult) - - # test Jtvec - v = np.random.rand(len(sim_ts) * survey.nD) - jtvec_full = r_serial_sim.Jtvec(t_model, v, f=f_serial) - jtvec_mult = r_parallel_sim.Jtvec(t_model, v, f=f_parallel) - np.testing.assert_allclose(jtvec_full, jtvec_mult) - - # test get diag - diag_full = r_serial_sim.getJtJdiag(t_model, f=f_serial) - diag_mult = r_parallel_sim.getJtJdiag(t_model, f=f_parallel) - np.testing.assert_allclose(diag_full, diag_mult) + sim_ts = np.linspace(0, 1, 6) + + repeat_mappings = [] + repeat_simulations = [] + eye = sp.eye(mesh.n_cells, mesh.n_cells) + for t in sim_ts: + ave_time = time_mesh.get_interpolation_matrix( + [ + t, + ] + ) + ave_full = sp.kron(ave_time, eye, format="csr") + repeat_mappings.append(maps.LinearMap(ave_full)) + + serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) + parallel_sim = MultiprocessingRepeatedSimulation(grav_sim, repeat_mappings) + t_model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) + + try: + # test field things + f_serial = serial_sim.fields(t_model) + f_parallel = parallel_sim.fields(t_model) + # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) + + d_full = serial_sim.dpred(t_model, f_serial) + d_repeat = parallel_sim.dpred(t_model, f_parallel) + np.testing.assert_equal(d_full, d_repeat) + + # test Jvec + u = np.random.rand(len(t_model)) + jvec_full = serial_sim.Jvec(t_model, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(t_model, u, f=f_parallel) + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = np.random.rand(len(sim_ts) * survey.nD) + jtvec_full = serial_sim.Jtvec(t_model, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(t_model, v, f=f_parallel) + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_sim.getJtJdiag(t_model, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(t_model, f=f_parallel) + np.testing.assert_allclose(diag_full, diag_mult) + except Exception as err: + raise err + finally: + parallel_sim.close() From aad965413568d719e0710bc65f152cb945d1ab3d Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 May 2023 19:43:48 -0700 Subject: [PATCH 095/455] flake --- tests/meta/test_multiprocessing_sim.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index fbd4bd6797..4b199429c0 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -5,7 +5,6 @@ from SimPEG import maps from discretize import TensorMesh import scipy.sparse as sp -import pytest from SimPEG.meta import ( MetaSimulation, @@ -206,7 +205,6 @@ def test_repeat_correctness(): sim_ts = np.linspace(0, 1, 6) repeat_mappings = [] - repeat_simulations = [] eye = sp.eye(mesh.n_cells, mesh.n_cells) for t in sim_ts: ave_time = time_mesh.get_interpolation_matrix( From af485d713d9fe63e1ea94cfa6324951aa2eced61 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 May 2023 15:06:40 -0700 Subject: [PATCH 096/455] Start refactoring ComboObjectiveFunction Refactor the constructor of ComboObjectiveFunction, moving the validation of the objective functions and multipliers to a private method, and simplifying how the nP is set. --- SimPEG/objective_function.py | 94 +++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b63d1bfb71..74c5674739 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -132,7 +132,7 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): num=num, expectedOrder=expectedOrder, plotIt=plotIt, - **kwargs + **kwargs, ) def test(self, x=None, num=4, plotIt=False, **kwargs): @@ -229,48 +229,16 @@ def __init__(self, objfcts=None, multipliers=None, **kwargs): if multipliers is None: multipliers = len(objfcts) * [1] - self._nP = "*" - - assert len(objfcts) == len(multipliers), ( - "Must have the same number of Objective Functions and Multipliers " - "not {} and {}".format(len(objfcts), len(multipliers)) - ) - - def validate_list(objfctlist, multipliers): - """ - ensure that the number of parameters expected by each objective - function is the same, ensure that if multpliers are supplied, that - list matches the length of the objective function list - """ - for fct, mult in zip(objfctlist, multipliers): - assert isinstance(fct, BaseObjectiveFunction), ( - "Unrecognized objective function type {} in objfcts. " - "All entries in objfcts must inherit from " - "ObjectiveFunction".format(fct.__class__.__name__) - ) - - assert type(mult) in self._multiplier_types, ( - "Objective Functions can only be multiplied by a " - "float, or a properties.Float, not a {}, {}".format( - type(mult), mult - ) - ) - - if fct.nP != "*": - if self._nP != "*": - assert self._nP == fct.nP, ( - "Objective Functions must all have the same " - "nP={}, not {}".format(self.nP, [f.nP for f in objfcts]) - ) - else: - self._nP = fct.nP - - validate_list(objfcts, multipliers) - + self._validate_objective_functions_and_multipliers(objfcts, multipliers) self.objfcts = objfcts self._multipliers = multipliers - super(ComboObjectiveFunction, self).__init__(**kwargs) + print("kwargs:", kwargs) + if "nP" not in kwargs: + number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] + if number_of_parameters: + kwargs["nP"] = number_of_parameters[0] + super().__init__(**kwargs) def __len__(self): return len(self.multipliers) @@ -390,6 +358,52 @@ def get_functions_of_type(self, fun_class) -> list: return [fun for fun in target if fun] + def _validate_objective_functions_and_multipliers( + self, objective_functions, multipliers + ): + """ + Check objective functions and multipliers passed to the constructor + + Check that the objective functions and the multipliers have the same + number of elements. Check if the objective functions and multipliers + have the right types. Check all objective functions have the same + number of parameters. + """ + # Check len of objective functions and multipliers + if len(objective_functions) != len(multipliers): + raise ValueError( + "Inconsistent number of elements between objective functions " + f"('{len(objective_functions)}') and multipliers " + f"('{len(multipliers)}'). They must have the same number of parameters." + ) + # Check types of objective functions + for function in objective_functions: + if not isinstance(function, BaseObjectiveFunction): + raise TypeError( + "Unrecognized objective function type " + f"{function.__class__.__name__} in 'objfcts'. " + "All objective functions must inherit from BaseObjectiveFunction." + ) + # Check if objective functions have the same number of parameters + number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] + if number_of_parameters: + all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) + if not all_equal: + np_list = [f.nP for f in objective_functions] + raise ValueError( + f"Invalid number of parameters '{np_list}' found in " + "objective functions. Except for the ones with '*', they all " + "must have the same number of parameters." + ) + # Check types of multipliers + for multiplier in multipliers: + if type(multiplier) not in self._multiplier_types: + valid_types = ", ".join(str(t) for t in self._multiplier_types) + raise TypeError( + f"Invalid multiplier '{multiplier}' of type '{type(multiplier)}'. " + "Objective functions can only be multiplied by " + valid_types + ) + class L2ObjectiveFunction(BaseObjectiveFunction): r""" From 7d948fdafbe22d0972144abaa8e00a3b1034e5a3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 May 2023 15:20:08 -0700 Subject: [PATCH 097/455] Remove _nP as class attribute of BaseObjectiveFunction Remove `nP` as a class attribute of BaseObjectiveFunction, and set it only in the constructor. --- SimPEG/objective_function.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 74c5674739..5ee4d324dc 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -28,11 +28,8 @@ class BaseObjectiveFunction(BaseSimPEG): _mapping = None #: An IdentityMap instance. _has_fields = False #: should we have the option to store fields - _nP = None #: number of parameters - def __init__(self, nP=None, **kwargs): - if nP is not None: - self._nP = nP + self._nP = nP set_kwargs(self, **kwargs) def __call__(self, x, f=None): From 826a1db4110c1d82c36a16413d67ed8fbe7c0b83 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 May 2023 16:05:33 -0700 Subject: [PATCH 098/455] Remove _multipliers class attribute and **kwargs Remove the _multipliers class attribute from ComboObjectiveFunction. Remove the kwargs in its constructor. Simplify how `nP` is defined, and change the order of the super() call and the definition of objfcts and multipliers attributes. --- SimPEG/objective_function.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 5ee4d324dc..41ab829cbe 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -217,26 +217,26 @@ class ComboObjectiveFunction(BaseObjectiveFunction): """ - _multiplier_types = (float, None, Zero, np.float64, int, np.integer) # Directive - _multipliers = None + _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, **kwargs): + def __init__(self, objfcts=None, multipliers=None): if objfcts is None: objfcts = [] if multipliers is None: multipliers = len(objfcts) * [1] - self._validate_objective_functions_and_multipliers(objfcts, multipliers) + + # Get number of parameters (nP) from objective functions + number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] + if number_of_parameters: + nP = number_of_parameters[0] + else: + nP = None + + super().__init__(nP=nP) self.objfcts = objfcts self._multipliers = multipliers - print("kwargs:", kwargs) - if "nP" not in kwargs: - number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] - if number_of_parameters: - kwargs["nP"] = number_of_parameters[0] - super().__init__(**kwargs) - def __len__(self): return len(self.multipliers) From 3786e36815cf4ed07850d0195fc4d18b024b52a0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 May 2023 16:07:30 -0700 Subject: [PATCH 099/455] Improve docstring of ComboObjectiveFunction --- SimPEG/objective_function.py | 44 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 41ab829cbe..7052bcddad 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -191,30 +191,26 @@ def __rdiv__(self, denominator): class ComboObjectiveFunction(BaseObjectiveFunction): """ - A composite objective function that consists of multiple objective - functions. Objective functions are stored in a list, and multipliers - are stored in a parallel list. - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = 2*phi1 + 3*phi2 - - is equivalent to - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = ObjectiveFunction.ComboObjectiveFunction( - [phi1, phi2], [2, 3] - ) - + Composite class for multiple objective functions + + A composite class for multiple objective functions. Each objective function + is accompanied by a multiplier. Both objective functions and multipliers + are stored in a list. + + Parameters + ---------- + objfcts : list or None, optional + List containing the objective functions that will live inside the + composite class. If ``None``, an empty list will be created. + multipliers : list or None, optional + List containing the multipliers for its respective objective function + in ``objfcts``. If ``None``, an empty list will be created. + + Examples + -------- + >>> objective_fun_a = BaseObjectiveFunction(nP=3) + >>> objective_fun_b = BaseObjectiveFunction(nP=3) + >>> combo = ComboObjectiveFunction([objective_fun_a, objective_fun_b], [1, 0.5]) """ _multiplier_types = (float, None, Zero, np.float64, int, np.integer) From d430eef8db2e42aee9f7790bf8c959a89f8ec069 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 May 2023 16:31:43 -0700 Subject: [PATCH 100/455] Split validators in three different methods Reuse multipliers validators in multipliers setter method. Fix mistake in docstring. --- SimPEG/objective_function.py | 72 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 7052bcddad..5102c2f87d 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -204,7 +204,8 @@ class ComboObjectiveFunction(BaseObjectiveFunction): composite class. If ``None``, an empty list will be created. multipliers : list or None, optional List containing the multipliers for its respective objective function - in ``objfcts``. If ``None``, an empty list will be created. + in ``objfcts``. If ``None``, a list full of ones with the same length + as ``objfcts`` will be created. Examples -------- @@ -216,11 +217,16 @@ class ComboObjectiveFunction(BaseObjectiveFunction): _multiplier_types = (float, None, Zero, np.float64, int, np.integer) def __init__(self, objfcts=None, multipliers=None): + # Define default lists if None if objfcts is None: objfcts = [] if multipliers is None: multipliers = len(objfcts) * [1] - self._validate_objective_functions_and_multipliers(objfcts, multipliers) + + # Validate inputs + self._check_length_objective_funcs_multipliers(objfcts, multipliers) + self._validate_objective_functions(objfcts) + self._validate_multipliers(multipliers) # Get number of parameters (nP) from objective functions number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] @@ -241,22 +247,18 @@ def __getitem__(self, key): @property def multipliers(self): + """ + Multipliers for each objective function + """ return self._multipliers @multipliers.setter def multipliers(self, value): - for val in value: - assert ( - type(val) in self._multiplier_types - ), "Multiplier must be in type {} not {}".format( - self._multiplier_types, type(val) - ) - - assert len(value) == len(self.objfcts), ( - "the length of multipliers should be the same as the number of" - " objective functions ({}), not {}".format(len(self.objfcts), len(value)) - ) - + """ + Set multipliers attribute after checking if they are valid + """ + self._validate_multipliers(value) + self._check_length_objective_funcs_multipliers(self.objfcts, value) self._multipliers = value def __call__(self, m, f=None): @@ -351,25 +353,13 @@ def get_functions_of_type(self, fun_class) -> list: return [fun for fun in target if fun] - def _validate_objective_functions_and_multipliers( - self, objective_functions, multipliers - ): + def _validate_objective_functions(self, objective_functions): """ - Check objective functions and multipliers passed to the constructor + Validate objective functions - Check that the objective functions and the multipliers have the same - number of elements. Check if the objective functions and multipliers - have the right types. Check all objective functions have the same - number of parameters. + Check if the objective functions have the right types, and if + they all have the same number of parameters. """ - # Check len of objective functions and multipliers - if len(objective_functions) != len(multipliers): - raise ValueError( - "Inconsistent number of elements between objective functions " - f"('{len(objective_functions)}') and multipliers " - f"('{len(multipliers)}'). They must have the same number of parameters." - ) - # Check types of objective functions for function in objective_functions: if not isinstance(function, BaseObjectiveFunction): raise TypeError( @@ -377,7 +367,6 @@ def _validate_objective_functions_and_multipliers( f"{function.__class__.__name__} in 'objfcts'. " "All objective functions must inherit from BaseObjectiveFunction." ) - # Check if objective functions have the same number of parameters number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] if number_of_parameters: all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) @@ -388,7 +377,13 @@ def _validate_objective_functions_and_multipliers( "objective functions. Except for the ones with '*', they all " "must have the same number of parameters." ) - # Check types of multipliers + + def _validate_multipliers(self, multipliers): + """ + Validate multipliers + + Check if the multipliers have the right types. + """ for multiplier in multipliers: if type(multiplier) not in self._multiplier_types: valid_types = ", ".join(str(t) for t in self._multiplier_types) @@ -397,6 +392,19 @@ def _validate_objective_functions_and_multipliers( "Objective functions can only be multiplied by " + valid_types ) + def _check_length_objective_funcs_multipliers( + self, objective_functions, multipliers + ): + """ + Check if objective functions and multipliers have the same length + """ + if len(objective_functions) != len(multipliers): + raise ValueError( + "Inconsistent number of elements between objective functions " + f"('{len(objective_functions)}') and multipliers " + f"('{len(multipliers)}'). They must have the same number of parameters." + ) + class L2ObjectiveFunction(BaseObjectiveFunction): r""" From 4c94094d20189410d6956b72f2705e9992549b48 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Tue, 30 May 2023 12:59:09 -0700 Subject: [PATCH 101/455] start of implementing permittivity in the EM problems --- .../frequency_domain/simulation.py | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 9c1372b80f..075ab13fc1 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -1,8 +1,11 @@ import numpy as np import scipy.sparse as sp +from scipy.constants import epsilon_0 from discretize.utils import Zero +from ... import props from ...data import Data +from ...base.pde_simulation import with_property_mass_matrices from ...utils import mkvc, validate_type from ..base import BaseEMSimulation from ..utils import omega @@ -16,6 +19,7 @@ ) +@with_property_mass_matrices("permittivity") class BaseFDEMSimulation(BaseEMSimulation): r""" We start by looking at Maxwell's equations in the electric @@ -53,9 +57,18 @@ class BaseFDEMSimulation(BaseEMSimulation): fieldsPair = FieldsFDEM - def __init__(self, mesh, survey=None, forward_only=False, **kwargs): + permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + + def __init__( + self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only + if permittivity is not None: + self.permittivity = permittivity + self.quasistatic = False + else: + self.quasistatic = True @property def survey(self): @@ -89,6 +102,25 @@ def forward_only(self): def forward_only(self, value): self._forward_only = validate_type("forward_only", value, bool) + @property + def quasistatic(self): + """If True, we assume the quasistatic approximation to Maxwell's + equations and neglect the role of dielectric permittivity in the + simulation. The default is True. + + Returns + ------- + bool + """ + return self._quasistatic + + @quasistatic.setter + def quasistatic(self, value): + if value is False and self.permittivity is None: + print("Permittivity not set. Setting to default free-space value.") + self.permittivity = epsilon_0 + self._quasistatic = validate_type("quasistatic", value, bool) + # @profile def fields(self, m=None): """ @@ -282,9 +314,13 @@ def getA(self, freq): MfMui = self.MfMui MeSigma = self.MeSigma + MePermittivity = self.MePermittivity C = self.mesh.edge_curl - return C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma + A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma + if self.quasistatic is False: + A = A - omega(freq) ** 2 * MePermittivity + return A def getADeriv_sigma(self, freq, u, v, adjoint=False): r""" @@ -328,9 +364,31 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) + def getADeriv_permittivity(self, freq, u, v, adjoint=False): + r""" + Product of the derivative of our system matrix with respect to the + permittivity model and a vector + + :param float freq: frequency + :param numpy.ndarray u: solution vector (nE,) + :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for + adjoint + :param bool adjoint: adjoint? + :rtype: numpy.ndarray + :return: derivative of the system matrix times a vector (nP,) or + adjoint (nD,) + """ + + if self.quasistatic is True: + return Zero() + dMe_dpermittivity_v = self.MePermittivityDeriv(u, v, adjoint) + return -omega(freq) ** 2 * dMe_dpermittivity_v + def getADeriv(self, freq, u, v, adjoint=False): - return self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui( - freq, u, v, adjoint + return ( + self.getADeriv_sigma(freq, u, v, adjoint) + + self.getADeriv_mui(freq, u, v, adjoint) + + self.getADeriv_permittivity(freq, u, v, adjoint) ) def getRHS(self, freq): From a79c657e4c98d0306c952741d3274171ed8200ff Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 30 May 2023 15:28:04 -0700 Subject: [PATCH 102/455] Refactor __add__ method of BaseObjectiveFunction Improve readability, raise TypeError after invalid class. --- SimPEG/objective_function.py | 51 +++++++++++++++++------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 5102c2f87d..b89946f567 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -143,38 +143,35 @@ def test(self, x=None, num=4, plotIt=False, **kwargs): __numpy_ufunc__ = True - def __add__(self, objfct2): - if isinstance(objfct2, Zero): + def __add__(self, other): + if isinstance(other, Zero): return self - - if not isinstance(objfct2, BaseObjectiveFunction): - raise Exception( - "Cannot add type {} to an objective function. Only " - "ObjectiveFunctions can be added together".format( - objfct2.__class__.__name__ - ) + if not isinstance(other, BaseObjectiveFunction): + raise TypeError( + f"Cannot add type '{other.__class__.__name__}' to an objective " + "function. Only ObjectiveFunctions can be added together." ) + objective_functions, multipliers = [], [] + for instance in (self, other): + if instance.__class__.__name__ == "ComboObjectiveFunction": + # Don't use `isinstance(instance, ComboObjectiveFunction)` + # here. Any child of ComboObjectiveFunction should be added as + # a whole, not unpacked in the resulting Combo class. + objective_functions += instance.objfcts + multipliers += instance.multipliers + else: + objective_functions.append(instance) + multipliers.append(1) + combo = ComboObjectiveFunction( + objfcts=objective_functions, multipliers=multipliers + ) + return combo - if ( - self.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(self, ComboObjectiveFunction): - self = 1 * self - - if ( - objfct2.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(objfct2, ComboObjectiveFunction): - objfct2 = 1 * objfct2 - - objfctlist = self.objfcts + objfct2.objfcts - multipliers = self.multipliers + objfct2.multipliers - - return ComboObjectiveFunction(objfcts=objfctlist, multipliers=multipliers) - - def __radd__(self, objfct2): - return self + objfct2 + def __radd__(self, other): + return self + other def __mul__(self, multiplier): - return ComboObjectiveFunction([self], [multiplier]) + return ComboObjectiveFunction(objfcts=[self], multipliers=[multiplier]) def __rmul__(self, multiplier): return self * multiplier From 39a5d72a16d8a55ed08132bb6cf774a8e281d64a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 30 May 2023 16:41:43 -0700 Subject: [PATCH 103/455] Refactor mapping as argument of the constructor Move the class attribute `mapping` to an argument of the constructor of BaseObjectiveFunction. --- SimPEG/objective_function.py | 4 ++-- SimPEG/regularization/base.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b89946f567..c18b30eeba 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -25,11 +25,11 @@ class BaseObjectiveFunction(BaseSimPEG): debug = False mapPair = IdentityMap #: Base class of expected maps - _mapping = None #: An IdentityMap instance. _has_fields = False #: should we have the option to store fields - def __init__(self, nP=None, **kwargs): + def __init__(self, nP=None, mapping=None, **kwargs): self._nP = nP + self._mapping = mapping set_kwargs(self, **kwargs) def __call__(self, x, f=None): diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 0979c8dcd1..884799a94b 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -58,10 +58,9 @@ def __init__( if active_cells is not None: self.active_cells = active_cells - self.mapping = mapping - - super().__init__(**kwargs) + super().__init__(nP=None, mapping=None, **kwargs) + self.mapping = mapping # Set mapping using the setter self.reference_model = reference_model self.units = units From 66aa4a94120e0501d2a8af291c62731563967555 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 30 May 2023 16:43:23 -0700 Subject: [PATCH 104/455] Start refactoring L2ObjectiveFunction Remove kwargs from the constructor, add nP and mapping as optional arguments. Add check in case nP mismatch the number of columns in W. --- SimPEG/objective_function.py | 17 +++++++++++------ tests/base/test_objective_function.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index c18b30eeba..87a5c24e48 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -412,17 +412,22 @@ class L2ObjectiveFunction(BaseObjectiveFunction): \phi = \frac{1}{2}||\mathbf{W} \mathbf{m}||^2 """ - def __init__(self, W=None, **kwargs): - super(L2ObjectiveFunction, self).__init__(**kwargs) - if W is not None: - if self.nP == "*": - self._nP = W.shape[1] + def __init__(self, nP=None, mapping=None, W=None): + # Check if nP and shape of W are consistent + if W is not None and nP is not None and nP != W.shape[1]: + raise ValueError( + f"Number of parameters nP ('{nP}') doesn't match the number of " + f"rows ('{W.shape[1]}') of the weights matrix W." + ) + super().__init__(nP=nP, mapping=mapping) + if W is not None and self.nP == "*": + self._nP = W.shape[1] self._W = W @property def W(self): """ - Weighting matrix. The default if not sepcified is an identity. + Weighting matrix. The default if not specified is an identity. """ if getattr(self, "_W", None) is None: if self._nC_residual != "*": diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 07d750a55d..4d331d0567 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -1,6 +1,7 @@ import numpy as np import scipy.sparse as sp import unittest +import pytest from SimPEG import utils, maps from SimPEG import objective_function @@ -313,6 +314,15 @@ def test_updating_multipliers(self): with self.assertRaises(Exception): phi3.multipliers = ["a", "b"] + def test_inconsistent_nparams_and_weights(self): + """ + Test if L2ObjectiveFunction raises error after nP != columns in W + """ + n_params = 9 + weights = np.zeros((5, n_params + 1)) + with pytest.raises(ValueError, match="Number of parameters nP"): + objective_function.L2ObjectiveFunction(nP=n_params, W=weights) + if __name__ == "__main__": unittest.main() From 80736e1f2255c7819d755f545ae5f3ad45a040d7 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 31 May 2023 11:23:08 -0400 Subject: [PATCH 105/455] Run black --- SimPEG/regularization/vector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index d86fa8e17d..cf64d79b3e 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from scipy.sparse import csr_matrix + class BaseVectorRegularization(BaseRegularization): """Base regularizer for models where each value is a vector. @@ -18,7 +19,6 @@ class BaseVectorRegularization(BaseRegularization): your model is made up of vector values in each cell or it is an anisotropic model. """ - @property def _weights_shapes(self) -> list[tuple[int]]: """Acceptable lengths for the weights @@ -29,7 +29,7 @@ def _weights_shapes(self) -> list[tuple[int]]: Each tuple represents accetable shapes for the weights """ mesh = self.regularization_mesh - + return [(mesh.nC,), (self.n_comp * mesh.nC,), (mesh.nC, self.n_comp)] @@ -70,6 +70,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): .. math:: \phi_{cross}(m) = \int_{V} ||\vec{m} \times \vec{m}_{ref}||^2 dV """ + def __init__( self, mesh, ref_dir, active_cells=None, mapping=None, weights=None, **kwargs ): @@ -146,7 +147,7 @@ def W(self): for value in self._weights.values(): if value.shape == (nC,): weights *= value - + elif value.size == (self.n_comp * nC,): weights *= np.linalg.norm( value.reshape((nC, self.n_comp), order="F"), axis=1 @@ -368,4 +369,3 @@ def __init__( mapping=mapping, **kwargs, ) - From ecf3d9039c899311e8c4a2984ccfa142bd7a9b19 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 31 May 2023 11:39:20 -0400 Subject: [PATCH 106/455] Re-run black --- SimPEG/directives/directives.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 3ae4de3bfb..e9ceb80e18 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2717,7 +2717,6 @@ def update(self): # Compute and sum root-mean squared sensitivities for all objective functions wr = np.zeros_like(self.invProb.model) for reg in self.reg.objfcts: - if isinstance(reg, BaseSimilarityMeasure): continue From 91ce8c5005cdd1062912c262d80c48cf13960745 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 31 May 2023 10:07:36 -0700 Subject: [PATCH 107/455] Rename mapPair class attribute to map_class --- SimPEG/objective_function.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 87a5c24e48..6b247bc058 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -24,7 +24,7 @@ class BaseObjectiveFunction(BaseSimPEG): counter = None debug = False - mapPair = IdentityMap #: Base class of expected maps + map_class = IdentityMap #: Base class of expected maps _has_fields = False #: should we have the option to store fields def __init__(self, nP=None, mapping=None, **kwargs): @@ -67,16 +67,16 @@ def mapping(self): """ if self._mapping is None: if self._nP is not None: - self._mapping = self.mapPair(nP=self.nP) + self._mapping = self.map_class(nP=self.nP) else: - self._mapping = self.mapPair() + self._mapping = self.map_class() return self._mapping @mapping.setter def mapping(self, value): - assert isinstance(value, self.mapPair), ( + assert isinstance(value, self.map_class), ( "mapping must be an instance of a {}, not a {}" - ).format(self.mapPair, value.__class__.__name__) + ).format(self.map_class, value.__class__.__name__) self._mapping = value @timeIt From 70ba3993a462ce9fd50aab527f631fdcfca07f4f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 31 May 2023 10:15:46 -0700 Subject: [PATCH 108/455] Move counter and debug to the constructor Move class attributes `counter` and `debug` as arguments to the constructor. --- SimPEG/objective_function.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 6b247bc058..23a460e84a 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -21,15 +21,14 @@ class BaseObjectiveFunction(BaseSimPEG): for building a data misfit, see :class:`SimPEG.DataMisfit.BaseDataMisfit`. """ - counter = None - debug = False - map_class = IdentityMap #: Base class of expected maps _has_fields = False #: should we have the option to store fields - def __init__(self, nP=None, mapping=None, **kwargs): + def __init__(self, nP=None, mapping=None, counter=None, debug=False, **kwargs): self._nP = nP self._mapping = mapping + self.counter = counter + self.debug = debug set_kwargs(self, **kwargs) def __call__(self, x, f=None): From 5ba4d3f15bdbdd95fb93aadb4acd5006c21237b6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 31 May 2023 11:54:36 -0700 Subject: [PATCH 109/455] Move has_fields, counter and debug as arguments Move the has_fields, counter and debug attributes as arguments for the constructor. Make `has_fields` public, since it's being used outside the class. Refactor the `__call__` and `deriv` methods by simplifying their code and using the new public `has_fields` attribute. --- SimPEG/data_misfit.py | 3 +- SimPEG/objective_function.py | 56 ++++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py index 12ca713062..d1ecb6e705 100644 --- a/SimPEG/data_misfit.py +++ b/SimPEG/data_misfit.py @@ -17,13 +17,12 @@ class BaseDataMisfit(L2ObjectiveFunction): """ def __init__(self, data, simulation, debug=False, counter=None, **kwargs): - super().__init__(**kwargs) + super().__init__(has_fields=True, debug=debug, counter=counter, **kwargs) self.data = data self.simulation = simulation self.debug = debug self.count = counter - self._has_fields = True @property def data(self): diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 23a460e84a..c29c608354 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -22,13 +22,21 @@ class BaseObjectiveFunction(BaseSimPEG): """ map_class = IdentityMap #: Base class of expected maps - _has_fields = False #: should we have the option to store fields - def __init__(self, nP=None, mapping=None, counter=None, debug=False, **kwargs): + def __init__( + self, + nP=None, + mapping=None, + has_fields=False, + counter=None, + debug=False, + **kwargs, + ): self._nP = nP self._mapping = mapping self.counter = counter self.debug = debug + self.has_fields = has_fields set_kwargs(self, **kwargs) def __call__(self, x, f=None): @@ -263,11 +271,11 @@ def __call__(self, m, f=None): multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct continue + if f is not None and objfct.has_fields: + objective_func_value = objfct(m, f=f[i]) else: - if f is not None and objfct._has_fields: - fct += multiplier * objfct(m, f=f[i]) - else: - fct += multiplier * objfct(m) + objective_func_value = objfct(m) + fct += multiplier * objective_func_value return fct def deriv(self, m, f=None): @@ -284,15 +292,12 @@ def deriv(self, m, f=None): multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct continue + if f is not None and objfct.has_fields: + aux = objfct.deriv(m, f=f[i]) else: - if f is not None and objfct._has_fields: - aux = objfct.deriv(m, f=f[i]) - if not isinstance(aux, Zero): - g += multiplier * aux - else: - aux = objfct.deriv(m) - if not isinstance(aux, Zero): - g += multiplier * aux + aux = objfct.deriv(m) + if not isinstance(aux, Zero): + g += multiplier * aux return g def deriv2(self, m, v=None, f=None): @@ -310,12 +315,11 @@ def deriv2(self, m, v=None, f=None): multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct continue + if f is not None and objfct.has_fields: + objfct_H = objfct.deriv2(m, v, f=f[i]) else: - if f is not None and objfct._has_fields: - objfct_H = objfct.deriv2(m, v, f=f[i]) - else: - objfct_H = objfct.deriv2(m, v) - H = H + multiplier * objfct_H + objfct_H = objfct.deriv2(m, v) + H = H + multiplier * objfct_H return H # This assumes all objective functions have a W. @@ -411,14 +415,24 @@ class L2ObjectiveFunction(BaseObjectiveFunction): \phi = \frac{1}{2}||\mathbf{W} \mathbf{m}||^2 """ - def __init__(self, nP=None, mapping=None, W=None): + def __init__( + self, + nP=None, + mapping=None, + W=None, + has_fields=False, + counter=None, + debug=False, + ): # Check if nP and shape of W are consistent if W is not None and nP is not None and nP != W.shape[1]: raise ValueError( f"Number of parameters nP ('{nP}') doesn't match the number of " f"rows ('{W.shape[1]}') of the weights matrix W." ) - super().__init__(nP=nP, mapping=mapping) + super().__init__( + nP=nP, mapping=mapping, has_fields=has_fields, debug=debug, counter=counter + ) if W is not None and self.nP == "*": self._nP = W.shape[1] self._W = W From 1dcd0125c96b0c19899fb75ca2fe87bb642de598 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 31 May 2023 12:01:08 -0700 Subject: [PATCH 110/455] Add docstring to __call__ methods --- SimPEG/objective_function.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index c29c608354..7f98cdb92a 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -40,6 +40,9 @@ def __init__( set_kwargs(self, **kwargs) def __call__(self, x, f=None): + """ + Evaluate the objective functions for a given model + """ raise NotImplementedError( "__call__ has not been implemented for {} yet".format( self.__class__.__name__ @@ -266,6 +269,9 @@ def multipliers(self, value): self._multipliers = value def __call__(self, m, f=None): + """ + Evaluate the objective functions for a given model + """ fct = 0.0 for i, phi in enumerate(self): multiplier, objfct = phi @@ -450,6 +456,9 @@ def W(self): return self._W def __call__(self, m): + """ + Evaluate the objective functions for a given model + """ r = self.W * (self.mapping * m) return 0.5 * r.dot(r) From 8a7461017cef574874c715c93a7fca62ce454787 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 31 May 2023 14:16:13 -0700 Subject: [PATCH 111/455] Generalize __add__ for any ComboObjectiveFunction Rewrite the `__add__` method of `BaseObjectiveFunction` so by default it performs the same task for any `ComboObjectiveFunction` or any of its child classes. Add a new `unpack_on_add` attribute to `ComboObjectiveFunction` to decide weather to unpack its objective functions after an addition, or if it should be added as a whole. Set `unpack_on_add=True` by default on `ComboObjectiveFunction`, but set it to `False` for regularization classes, like `WeightedLeastSquares` and `PGI`. Add tests for the new changes. --- SimPEG/objective_function.py | 143 ++++++++++++++++---------- SimPEG/regularization/base.py | 2 +- SimPEG/regularization/pgi.py | 2 +- tests/base/test_objective_function.py | 63 ++++++++++++ 4 files changed, 156 insertions(+), 54 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b63d1bfb71..93e21e60d7 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -132,7 +132,7 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): num=num, expectedOrder=expectedOrder, plotIt=plotIt, - **kwargs + **kwargs, ) def test(self, x=None, num=4, plotIt=False, **kwargs): @@ -146,32 +146,26 @@ def test(self, x=None, num=4, plotIt=False, **kwargs): __numpy_ufunc__ = True - def __add__(self, objfct2): - if isinstance(objfct2, Zero): + def __add__(self, other): + if isinstance(other, Zero): return self - - if not isinstance(objfct2, BaseObjectiveFunction): - raise Exception( - "Cannot add type {} to an objective function. Only " - "ObjectiveFunctions can be added together".format( - objfct2.__class__.__name__ - ) + if not isinstance(other, BaseObjectiveFunction): + raise TypeError( + f"Cannot add type '{other.__class__.__name__}' to an objective " + "function. Only ObjectiveFunctions can be added together." ) - - if ( - self.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(self, ComboObjectiveFunction): - self = 1 * self - - if ( - objfct2.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(objfct2, ComboObjectiveFunction): - objfct2 = 1 * objfct2 - - objfctlist = self.objfcts + objfct2.objfcts - multipliers = self.multipliers + objfct2.multipliers - - return ComboObjectiveFunction(objfcts=objfctlist, multipliers=multipliers) + objective_functions, multipliers = [], [] + for instance in (self, other): + if isinstance(instance, ComboObjectiveFunction) and instance._unpack_on_add: + objective_functions += instance.objfcts + multipliers += instance.multipliers + else: + objective_functions.append(instance) + multipliers.append(1) + combo = ComboObjectiveFunction( + objfcts=objective_functions, multipliers=multipliers + ) + return combo def __radd__(self, objfct2): return self + objfct2 @@ -183,47 +177,91 @@ def __rmul__(self, multiplier): return self * multiplier def __div__(self, denominator): - return self.__mul__(1.0 / denominator) + return self * (1.0 / denominator) def __truediv__(self, denominator): - return self.__mul__(1.0 / denominator) + return self * (1.0 / denominator) def __rdiv__(self, denominator): - return self.__mul__(1.0 / denominator) + return self * (1.0 / denominator) class ComboObjectiveFunction(BaseObjectiveFunction): """ - A composite objective function that consists of multiple objective - functions. Objective functions are stored in a list, and multipliers - are stored in a parallel list. - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = 2*phi1 + 3*phi2 - - is equivalent to - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = ObjectiveFunction.ComboObjectiveFunction( - [phi1, phi2], [2, 3] - ) + Composite for multiple objective functions + + A composite class for multiple objective functions. Each objective function + is accompanied by a multiplier. Both objective functions and multipliers + are stored in a list. + + Parameters + ---------- + objfcts : list or None, optional + List containing the objective functions that will live inside the + composite class. If ``None``, an empty list will be created. + multipliers : list or None, optional + List containing the multipliers for its respective objective function + in ``objfcts``. If ``None``, a list full of ones with the same length + as ``objfcts`` will be created. + unpack_on_add : bool, optional + Weather to unpack the multiple objective functions when adding them to + another objective function, or to add them as a whole. + + Examples + -------- + Build a simple combo objective function: + + >>> objective_fun_a = L2ObjectiveFunction(nP=3) + >>> objective_fun_b = L2ObjectiveFunction(nP=3) + >>> combo = ComboObjectiveFunction([objective_fun_a, objective_fun_b], [1, 0.5]) + >>> print(len(combo)) + 2 + >>> print(combo.multipliers) + [1, 0.5] + + Combo objective functions are also created after adding two objective functions: + + >>> combo = 2 * objective_fun_a + 3.5 * objective_fun_b + >>> print(len(combo)) + 2 + >>> print(combo.multipliers) + [2, 3.5] + + We could add two combo objective functions as well: + + >>> objective_fun_c = L2ObjectiveFunction(nP=3) + >>> objective_fun_d = L2ObjectiveFunction(nP=3) + >>> combo_1 = 4.3 * objective_fun_a + 3 * objective_fun_b + >>> combo_2 = 1.5 * objective_fun_c + 0.5 * objective_fun_d + >>> combo = combo_1 + combo_2 + >>> print(len(combo)) + 4 + >>> print(combo.multipliers) + [4.3, 3, 1.5, 0.5] + + We can choose to not unpack the objective functions when creating the + combo. For example: + + >>> objective_fun_a = L2ObjectiveFunction(nP=3) + >>> objective_fun_b = L2ObjectiveFunction(nP=3) + >>> objective_fun_c = L2ObjectiveFunction(nP=3) + >>> + >>> # Create a ComboObjectiveFunction that won't unpack + >>> combo_1 = ComboObjectiveFunction( + ... objfcts=[objective_fun_a, objective_fun_b], + ... multipliers=[0.1, 1.2], + ... unpack_on_add=False, + ... ) + >>> combo_2 = combo_1 + objective_fun_c + >>> print(len(combo_2)) + 2 """ _multiplier_types = (float, None, Zero, np.float64, int, np.integer) # Directive _multipliers = None - def __init__(self, objfcts=None, multipliers=None, **kwargs): + def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True, **kwargs): if objfcts is None: objfcts = [] if multipliers is None: @@ -269,6 +307,7 @@ def validate_list(objfctlist, multipliers): self.objfcts = objfcts self._multipliers = multipliers + self._unpack_on_add = unpack_on_add super(ComboObjectiveFunction, self).__init__(**kwargs) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 0979c8dcd1..00c25ffb5c 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -781,7 +781,7 @@ def __init__( ) else: objfcts = kwargs.pop("objfcts") - super().__init__(objfcts=objfcts, **kwargs) + super().__init__(objfcts=objfcts, unpack_on_add=False, **kwargs) self.mapping = mapping self.reference_model = reference_model self.reference_model_in_smooth = reference_model_in_smooth diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index fc38cb0e3d..45d8f43556 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -748,7 +748,7 @@ def __init__( ) ] - super().__init__(objfcts=objfcts) + super().__init__(objfcts=objfcts, unpack_on_add=False) self.reference_model_in_smooth = reference_model_in_smooth self.alpha_pgi = alpha_pgi diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 07d750a55d..90bdcfdae7 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -1,5 +1,6 @@ import numpy as np import scipy.sparse as sp +import pytest import unittest from SimPEG import utils, maps @@ -314,5 +315,67 @@ def test_updating_multipliers(self): phi3.multipliers = ["a", "b"] +class TestOperationsComboObjectiveFunctions: + """Test arithmetic operations involving ComboObjectiveFunction""" + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_mul(self, unpack_on_add): + """Test if ComboObjectiveFunction multiplication works as expected""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + combo = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_mul = 3.5 * combo + assert len(combo_mul) == 1 + assert combo_mul.multipliers == [3.5] + assert combo_mul.objfcts == [combo] + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_add(self, unpack_on_add): + """Test if ComboObjectiveFunction addition works as expected""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo_1 = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_2 = phi3 + combo_1 + if unpack_on_add: + assert len(combo_2) == 3 + assert combo_2.multipliers == [1, 2, 3] + assert combo_2.objfcts == [phi3, phi1, phi2] + else: + assert len(combo_2) == 2 + assert combo_2.multipliers == [1, 1] + assert combo_2.objfcts == [phi3, combo_1] + combo_1 = combo_2.objfcts[1] + assert combo_1.multipliers == [2, 3] + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_add_and_mul(self, unpack_on_add): + """Test ComboObjectiveFunction addition with multiplication""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo_1 = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_2 = 5 * phi3 + 1.2 * combo_1 + if unpack_on_add: + assert len(combo_2) == 3 + assert combo_2.multipliers == [5, 1.2 * 2, 1.2 * 3] + assert combo_2.objfcts == [phi3, phi1, phi2] + else: + assert len(combo_2) == 2 + assert combo_2.multipliers == [5, 1] + assert combo_2.objfcts == [phi3, combo_1] + combo_1 = combo_2.objfcts[1] + assert combo_1.multipliers == [1.2 * 2, 1.2 * 3] + + if __name__ == "__main__": unittest.main() From 06aebe9e2d12a329f15cf9114e0ecb49d6e860c7 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 31 May 2023 14:34:13 -0700 Subject: [PATCH 112/455] updates to the permittivity implimentation --- .../frequency_domain/simulation.py | 119 ++++++++++-------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 075ab13fc1..fbe3b18a3f 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -18,8 +18,9 @@ Fields3DCurrentDensity, ) +import warnings + -@with_property_mass_matrices("permittivity") class BaseFDEMSimulation(BaseEMSimulation): r""" We start by looking at Maxwell's equations in the electric @@ -57,18 +58,9 @@ class BaseFDEMSimulation(BaseEMSimulation): fieldsPair = FieldsFDEM - permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - - def __init__( - self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs - ): + def __init__(self, mesh, survey=None, forward_only=False, **kwargs): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only - if permittivity is not None: - self.permittivity = permittivity - self.quasistatic = False - else: - self.quasistatic = True @property def survey(self): @@ -102,25 +94,6 @@ def forward_only(self): def forward_only(self, value): self._forward_only = validate_type("forward_only", value, bool) - @property - def quasistatic(self): - """If True, we assume the quasistatic approximation to Maxwell's - equations and neglect the role of dielectric permittivity in the - simulation. The default is True. - - Returns - ------- - bool - """ - return self._quasistatic - - @quasistatic.setter - def quasistatic(self, value): - if value is False and self.permittivity is None: - print("Permittivity not set. Setting to default free-space value.") - self.permittivity = epsilon_0 - self._quasistatic = validate_type("quasistatic", value, bool) - # @profile def fields(self, m=None): """ @@ -269,6 +242,7 @@ def getSourceTerm(self, freq): ############################################################################### +@with_property_mass_matrices("permittivity") class Simulation3DElectricField(BaseFDEMSimulation): r""" By eliminating the magnetic flux density using @@ -298,6 +272,15 @@ class Simulation3DElectricField(BaseFDEMSimulation): _formulation = "EB" fieldsPair = Fields3DElectricField + permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") + + def __init__( + self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + ): + super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) + self.permittivity = permittivity + def getA(self, freq): r""" System matrix @@ -314,11 +297,13 @@ def getA(self, freq): MfMui = self.MfMui MeSigma = self.MeSigma - MePermittivity = self.MePermittivity + C = self.mesh.edge_curl A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma - if self.quasistatic is False: + + if getattr(self, "permittivity", None) is not None: + MePermittivity = self.MePermittivity A = A - omega(freq) ** 2 * MePermittivity return A @@ -364,31 +349,31 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) - def getADeriv_permittivity(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - permittivity model and a vector - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) - """ - - if self.quasistatic is True: - return Zero() - dMe_dpermittivity_v = self.MePermittivityDeriv(u, v, adjoint) - return -omega(freq) ** 2 * dMe_dpermittivity_v + # def getADeriv_permittivity(self, freq, u, v, adjoint=False): + # r""" + # Product of the derivative of our system matrix with respect to the + # permittivity model and a vector + + # :param float freq: frequency + # :param numpy.ndarray u: solution vector (nE,) + # :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for + # adjoint + # :param bool adjoint: adjoint? + # :rtype: numpy.ndarray + # :return: derivative of the system matrix times a vector (nP,) or + # adjoint (nD,) + # """ + + # if getattr(self, "permittivityMap", None) is not None: + # return Zero() + # dMe_dpermittivity_v = self.MePermittivityDeriv(u, v, adjoint) + # return -omega(freq) ** 2 * dMe_dpermittivity_v def getADeriv(self, freq, u, v, adjoint=False): return ( self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui(freq, u, v, adjoint) - + self.getADeriv_permittivity(freq, u, v, adjoint) + # + self.getADeriv_permittivity(freq, u, v, adjoint) ) def getRHS(self, freq): @@ -610,6 +595,7 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): ############################################################################### +@with_property_mass_matrices("permittivity") class Simulation3DCurrentDensity(BaseFDEMSimulation): r""" We eliminate :math:`mathbf{h}` using @@ -641,6 +627,27 @@ class Simulation3DCurrentDensity(BaseFDEMSimulation): _formulation = "HJ" fieldsPair = Fields3DCurrentDensity + permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + + def __init__( + self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + ): + super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) + self.permittivity = permittivity + + # def _clear_on_rho_update(self): + # return ( + # super()._clear_on_rho_update + self._Mfrho_permittivity + # ) + + @property + def _Mf_rho_permittivity(self): + """ + Face inner product matrix with permittivity and resistivity + """ + # todo: cache, but clear on update to rho + return self.mesh.get_face_inner_product(self.permittivity * self.rho) + def getA(self, freq): r""" System matrix @@ -662,6 +669,9 @@ def getA(self, freq): A = C * MeMuI * C.T.tocsr() * MfRho + iomega + if getattr(self, "permittivity", None) is not None: + A = A - omega(freq) ** 2 * self.MfI * self._Mf_rho_permittivity + if self._makeASymmetric is True: return MfRho.T.tocsr() * A return A @@ -716,6 +726,11 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return Aderiv def getADeriv(self, freq, u, v, adjoint=False): + if getattr(self, "permittivity", None) is not None: + warnings.warn( + "Derivatives not yet implemented for simulations that include permittivity" + ) + if adjoint and self._makeASymmetric: v = self.MfRho * v From 017110a509f64dd686b6c565c633e6cc371fec2b Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 1 Jun 2023 09:14:37 -0400 Subject: [PATCH 113/455] Fix tests --- SimPEG/regularization/vector.py | 25 +++++++++++++++++-------- tests/base/test_regularization.py | 16 ++-------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index cf64d79b3e..0ae8c17058 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -19,6 +19,13 @@ class BaseVectorRegularization(BaseRegularization): your model is made up of vector values in each cell or it is an anisotropic model. """ + @property + def n_comp(self): + """Number of components in the model.""" + if self.mapping.shape[0] == "*": + return self.regularization_mesh.dim + return int(self.mapping.shape[0] / self.regularization_mesh.nC) + @property def _weights_shapes(self) -> list[tuple[int]]: """Acceptable lengths for the weights @@ -104,13 +111,12 @@ def ref_dir(self, value): mesh = self.regularization_mesh nC = mesh.nC value = np.asarray(value) - if value.shape != (nC, self.n_comp): - if value.shape == (self.n_comp,): + if value.shape != (nC, mesh.dim): + if value.shape == (mesh.dim,): # expand it out for each mesh cell value = np.tile(value, (nC, 1)) else: - raise ValueError(f"ref_dir must be shape {(nC, self.n_comp)}") - + raise ValueError(f"ref_dir must be shape {(nC, mesh.dim)}") self._ref_dir = value R0 = sp.diags(value[:, 0]) @@ -147,13 +153,16 @@ def W(self): for value in self._weights.values(): if value.shape == (nC,): weights *= value - - elif value.size == (self.n_comp * nC,): + elif value.size == mesh.dim * nC: weights *= np.linalg.norm( - value.reshape((nC, self.n_comp), order="F"), axis=1 + value.reshape((nC, mesh.dim), order="F"), axis=1 ) weights = np.sqrt(weights) - self._W = sp.diags(np.r_[weights, weights, weights], format="csr") + if mesh.dim == 2: + diag = weights + else: + diag = np.r_[weights, weights, weights] + self._W = sp.diags(diag, format="csr") return self._W diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 4c8ea53c42..d27606f231 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -560,22 +560,10 @@ def test_vector_amplitude(self): with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): regularization.VectorAmplitude("abc") - with pytest.raises(TypeError, match="A 'mapping' of type"): - regularization.VectorAmplitude(mesh, maps.IdentityMap(mesh)) - - reg = regularization.VectorAmplitude(mesh) - assert len(reg.objfcts[0].mapping.maps) == 1 - - with pytest.raises(ValueError, match="All models must be the same size!"): - wires = ((f"wire{ind}", mesh.nC + ind) for ind in range(n_comp)) - regularization.VectorAmplitude(mesh, maps.Wires(*wires)) - - wires = ((f"wire{ind}", mesh.nC) for ind in range(n_comp)) - - reg = regularization.VectorAmplitude(mesh, maps.Wires(*wires)) + reg = regularization.VectorAmplitude(mesh, maps.IdentityMap(nP=n_comp*mesh.nC)) with pytest.raises( - ValueError, match=f"must be a tuple of len\({n_comp}\)" # noqa: W605 + ValueError, match=f"'weights' must be one of" # noqa: W605 ): reg.set_weights(abc=(1.0, 1.0)) From ff23ad4dbd57c0f75680555ef3c22410c2614d5a Mon Sep 17 00:00:00 2001 From: fourndo Date: Thu, 1 Jun 2023 10:28:50 -0400 Subject: [PATCH 114/455] Black on tests --- tests/base/test_regularization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index d27606f231..497d486780 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -560,11 +560,11 @@ def test_vector_amplitude(self): with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): regularization.VectorAmplitude("abc") - reg = regularization.VectorAmplitude(mesh, maps.IdentityMap(nP=n_comp*mesh.nC)) + reg = regularization.VectorAmplitude( + mesh, maps.IdentityMap(nP=n_comp * mesh.nC) + ) - with pytest.raises( - ValueError, match=f"'weights' must be one of" # noqa: W605 - ): + with pytest.raises(ValueError, match=f"'weights' must be one of"): # noqa: W605 reg.set_weights(abc=(1.0, 1.0)) np.testing.assert_almost_equal( From ef224b3a248fb21d2ce2d1487c72d13bb56028ed Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 1 Jun 2023 09:31:52 -0700 Subject: [PATCH 115/455] Test addition of multiple base objective functions --- tests/base/test_objective_function.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 90bdcfdae7..b4449fa4f0 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -354,6 +354,17 @@ def test_add(self, unpack_on_add): combo_1 = combo_2.objfcts[1] assert combo_1.multipliers == [2, 3] + def test_add_multiple_terms(self, unpack_on_add): + """Test addition of multiple BaseObjectiveFunctions""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo = 1.1 * phi1 + 1.2 * phi2 + 1.3 * phi3 + assert len(combo) == 3 + assert combo.multipliers == [1.1, 1.2, 1.3] + assert combo.objfcts == [phi1, phi2, phi3] + @pytest.mark.parametrize("unpack_on_add", (True, False)) def test_add_and_mul(self, unpack_on_add): """Test ComboObjectiveFunction addition with multiplication""" From 16dc5a4f73ec99ce2e5d70ed3ca8230f0c272fe9 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 1 Jun 2023 10:40:59 -0700 Subject: [PATCH 116/455] clean --- tests/meta/test_multiprocessing_sim.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 4b199429c0..52712bac61 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -194,24 +194,13 @@ def test_repeat_correctness(): mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 ) - time_mesh = TensorMesh( - [ - 8, - ], - origin=[ - 0, - ], - ) + time_mesh = TensorMesh([8], origin=[0]) sim_ts = np.linspace(0, 1, 6) repeat_mappings = [] eye = sp.eye(mesh.n_cells, mesh.n_cells) for t in sim_ts: - ave_time = time_mesh.get_interpolation_matrix( - [ - t, - ] - ) + ave_time = time_mesh.get_interpolation_matrix([t]) ave_full = sp.kron(ave_time, eye, format="csr") repeat_mappings.append(maps.LinearMap(ave_full)) From cb13eeffbe96ef594d768204be2dad5d2033b898 Mon Sep 17 00:00:00 2001 From: dccowan Date: Thu, 1 Jun 2023 10:41:02 -0700 Subject: [PATCH 117/455] first pass at joint regularization documentation --- SimPEG/regularization/base.py | 118 ++++------- SimPEG/regularization/cross_gradient.py | 203 ++++++++++++++----- SimPEG/regularization/jtv.py | 153 ++++++++++++-- SimPEG/regularization/regularization_mesh.py | 195 ++++++++++++++---- 4 files changed, 481 insertions(+), 188 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 7d5231f97a..bb9ff365f4 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -19,8 +19,7 @@ class BaseRegularization(BaseObjectiveFunction): """Base regularization class. The ``BaseRegularization`` class defines properties and methods inherited by - SimPEG regularization classes. It is not directly used to constrain - the inversions. + SimPEG regularization classes. It is not directly used to constrain inversions. Parameters ---------- @@ -402,12 +401,17 @@ def _delta_m(self, m) -> np.ndarray: @utils.timeIt def __call__(self, m): - r""" - We use a weighted 2-norm objective function + """Evaluate the regularization function for the model provided. - .. math:: + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. - \gamma (m) = \frac{1}{2} \| \mathbf{W} \mathbf{f(m)} \|_2^2 + Returns + ------- + float + The regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) return 0.5 * r.dot(r) @@ -422,41 +426,40 @@ def f_m_deriv(self, m) -> csr_matrix: @utils.timeIt def deriv(self, m) -> np.ndarray: - r"""Gradient of the regularization function evaluated for the model provided. + r"""Jacobian of the regularization function evaluated for the model provided. - Where :math:`\gamma (\mathbf{m})` represents the discrete regularization function, - this method returns the derivative with respect to the model parameters: + Where :math:`\gamma (\mathbf{m})` is the discrete regularization function, + this method returns the derivative (Jacobian) with respect to the model parameters: .. math:: - \frac{\partial \gamma}{\partial \mathbf{m}} \bigg |_\mathbf{m} + \frac{\partial \gamma}{\partial \mathbf{m}} \, \bigg |_\mathbf{m} evaluated at the model :math:`\mathbf{m}` provided. Parameters ---------- (n_param, ) numpy.ndarray - The model for which the gradient is evaluated. + The model for which the Jacobian is evaluated. Returns ------- (n_param, ) numpy.ndarray - The gradient of the regularization function evaluated for the model provided. - + The Jacobian of the regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) return self.f_m_deriv(m).T * (self.W.T * r) @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: - r"""Second derivative of the regularization function evaluated for the model provided. + r"""Hessian of the regularization function evaluated for the model provided. - Where :math:`\gamma (\mathbf{m})` represents the discrete regularization function, + Where :math:`\gamma (\mathbf{m})` is the discrete regularization function, this method returns the second derivative (Hessian) with respect to the model parameters: .. math:: \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} - or the second-derivative multiplied by a given vector :math:`(\mathbf{v})` + or the second-derivative (Hessian) multiplied by a given vector :math:`(\mathbf{v})` .. math:: \bigg [ \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} @@ -466,16 +469,16 @@ def deriv2(self, m, v=None) -> csr_matrix: Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the gradient is evaluated. + The model for which the Hessian is evaluated. v : None, (n_param, ) numpy.ndarray (optional) - A vector + A vector. Returns ------- (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the second-derivative of the regularization + If the input argument *v* is ``None``, the Hessian of the regularization function for the model provided is returned. If *v* is not ``None``, - the second-derivative multiplied by the vector provided is returned. + the Hessian multiplied by the vector provided is returned. """ f_m_deriv = self.f_m_deriv(m) @@ -525,7 +528,7 @@ class Smallness(BaseRegularization): We define the regularization function for smallness as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w( \mathscr{r}) \, + \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` @@ -717,7 +720,7 @@ class SmoothnessFirstOrder(BaseRegularization): the starting model. To include the reference model in the regularization, the `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. + Whether to include the reference model in the smoothness regularization. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -1048,8 +1051,8 @@ def orientation(self): Returns ------- - str - The direction along which smoothness is enforced. On of {'x','y','z'} + {'x','y','z'} + The direction along which smoothness is enforced. """ return self._orientation @@ -1084,7 +1087,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): the starting model. To include the reference model in the regularization, the `reference_model_in_smooth` property must be set to ``True``. reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. + Whether to include the reference model in the smoothness regularization. units : None, str Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. @@ -2153,26 +2156,20 @@ def mapping(self, mapping: maps.IdentityMap): # # ############################################################################### class BaseSimilarityMeasure(BaseRegularization): - """Base class for the similarity term in joint inversions. - - The ``BaseSimilarityMeasure`` assumes two different geophysical models through one similarity term. - Inherit this for building your own similarity term. - However, if you wish to combine more than two models, e.g., 3 models, - you may want to add a total of three coupling terms: - - e.g., lambda1*(m1, m2) + lambda2*(m1, m3) + lambda3*(m2, m3) + """Base class for joint inversion regularization. - where, lambdas are weights for coupling terms. m1, m2 and m3 indicate - three different models. + The ``BaseSimilarityMeasure`` class defines properties and methods used + by regularization classes for joint inversion. It is not directly used to + constrain inversions. Parameters ---------- mesh : SimPEG.regularization.RegularizationMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - mapping : SimPEG.maps.WireMap - Wire map connecting physical properties defined on active cells of the regularization mesh - to the entire model. + wire_map : SimPEG.maps.WireMap + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. """ def __init__(self, mesh, wire_map, **kwargs): @@ -2181,13 +2178,12 @@ def __init__(self, mesh, wire_map, **kwargs): @property def wire_map(self): - """Wire map connecting physical properties to the entire model. + """Mapping from model to physical properties defined on the regularization mesh. Returns ------- SimPEG.maps.WireMap - Wire map connecting physical properties defined on the regularization - mesh to the entire model. + Mapping from model to physical properties defined on the regularization mesh. """ return self._wire_map @@ -2216,22 +2212,7 @@ def nP(self): return self.wire_map.nP def deriv(self, model): - """First derivative of the coupling term with respect to individual models. - - Where :math:`k` is the number of models we are inverting for and :math:`M` is the number - of cells in each model, this method returns a vector of length :math:`kM`. - - Parameters - ---------- - model : numpy.ndarray - The model. - - Returns - ------- - numpy.ndarray - First derivative of the coupling term with respect to individual models. - - """ + """Not implemented for ``BaseSimilarityMeasure`` class.""" raise NotImplementedError( "The method deriv has not been implemented for {}".format( self.__class__.__name__ @@ -2239,26 +2220,7 @@ def deriv(self, model): ) def deriv2(self, model, v=None): - """Second derivative of the coupling term with respect to individual models. - - Parameters - ---------- - model : numpy.ndarray - The model. - v : numpy.ndarray, optional - A vector. - - Returns - ------- - numpy.ndarray or scipy.sparse.csr_matrix - Where :math:`k` is the number of models we are inverting for and :math:`M` is - the number of cells in each model, this method returns: - - - an array of dimensions (k*M, ) when `v` is not ``None``. - - a sparse matrix of dimensions (k*M, k*M) when `v` is ``None``. - - - """ + """Not implemented for ``BaseSimilarityMeasure`` class.""" raise NotImplementedError( "The method _deriv2 has not been implemented for {}".format( self.__class__.__name__ @@ -2273,7 +2235,7 @@ def _nC_residual(self): return self.wire_map.nP def __call__(self, model): - """Returns the computed value of the coupling term.""" + """Not implemented for ``BaseSimilarityMeasure`` class.""" raise NotImplementedError( "The method __call__ has not been implemented for {}".format( self.__class__.__name__ diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index e06c81a84c..90740e5ffb 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -13,14 +13,119 @@ class CrossGradient(BaseSimilarityMeasure): - r""" - The cross-gradient constraint for joint inversions. + r"""Cross-gradient regularization for joint inversion. + + ``CrossGradient`` regularization is used to ensure the location and orientation of non-zero + gradients in the recovered model are consistent across two physical property distributions. + For joint inversion involving three or more physical properties, a separate instance of + ``CrossGradient`` must be created for each physical property pair and added to the total + regularization as a weighted sum. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : SimPEG.maps.WireMap + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + approx_hessian : bool + Whether to use the semi-positive definate approximation for the Hessian. + + Notes + ----- + Consider the case where the model is comprised of two physical properties + :math:`m_1` and :math:`m_2`. Here, we define the regularization + function for cross-gradient as (`Haber and Gazit, 2013 `__): + + .. math:: + \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \Big | \nabla m_1 \, \times \, \nabla m_2 \, \Big |^2 \, dv + + where :math:`w(r)` is a user-defined weighting function. + Using the identity :math:`| \vec{a} \times \vec{b} |^2 = | \vec{a} |^2 | \vec{b} |^2 - (\vec{a} \cdot \vec{b})^2`, + the regularization function can be re-expressed as: + + .. math:: + \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, + \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 + - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discretized approximation for the regularization + function (objective function) is given by: + + .. math:: + \gamma (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ + \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 + - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] + + where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. + + In practice, we define the model :math:`\mathbf{m}` as a discrete + vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + where :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}` are the discrete representations + of the respective physical properties on the mesh. The discrete regularization function + is therefore equivalent to an objective function of the form: + + .. math:: + \gamma (\mathbf{m}) = + \frac{1}{2} \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T + \Big [ \mathbf{W A} \big ( \mathbf{G \, m_2} \big )^2 \Big ] + - \frac{1}{2} \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) + \odot \big ( \mathbf{G \, m_2} \big ) \Big ] \bigg \|^2 + + where exponents are computed elementwise, + + - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), + - :math:`\mathbf{A}` averages vectors from faces to cell centers, and + - :math:`\mathbf{W}` is the weighting matrix. + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the objective function is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply weights within the regularization is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = CrossGradient(mesh, wire_map, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: - ..math:: - \phi_c(\mathbf{m_1},\mathbf{m_2}) = \lambda \sum_{i=1}^{M} \| - \nabla \mathbf{m_1}_i \times \nabla \mathbf{m_2}_i \|^2 + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: - All methods assume that we are working with two models only. + >>> reg.get_weights('volume') """ @@ -37,10 +142,12 @@ def __init__(self, mesh, wire_map, approx_hessian=True, **kwargs): @property def approx_hessian(self): - """whether to use the semi-positive definate approximation for the hessian. + """Whether to use the semi-positive definate approximation for the Hessian. + Returns ------- bool + Whether to use the semi-positive definate approximation for the Hessian. """ return self._approx_hessian @@ -82,23 +189,22 @@ def _calculate_gradient(self, model, normalized=False, rtol=1e-6): return gradient def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): - """ - Calculates the cross-gradients of the models at each cell center. + r"""Calculates the norm of the cross-gradient vectors at cell centers. Parameters ---------- model : numpy.ndarray The input model, which will be automatically separated into the two - parameters internally + parameters internally. normalized : bool, optional - Whether to normalize the gradient + Whether to normalize the cross-gradients. rtol : float, optional - relative cuttoff for small gradients in the normalization + relative cuttoff for small gradients in the normalization. Returns ------- - cross_grad : numpy.ndarray - The norm of the cross gradient vector in each active cell. + numpy.ndarray + Calculates the norm of the cross-gradient vectors at cell centers. """ m1, m2 = self.wire_map * model # Compute the gradients and concatenate components. @@ -113,28 +219,22 @@ def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): return cross_prod def __call__(self, model): - r""" - Computes the sum of all cross-gradient values at all cell centers. - - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] - :param bool normalized: returns value of normalized cross-gradient if True - - :rtype: float - :returns: the computed value of the cross-gradient term. + """Evaluate the cross-gradient regularization function for the model provided. + See the *Notes* section of the documentation for the :class:`CrossGradient` class + for a full description of the regularization function. - ..math:: - - \phi_c(\mathbf{m_1},\mathbf{m_2}) - = \lambda \sum_{i=1}^{M} \|\nabla \mathbf{m_1}_i \times \nabla \mathbf{m_2}_i \|^2 - = \sum_{i=1}^{M} \|\nabla \mathbf{m_1}_i\|^2 \ast \|\nabla \mathbf{m_2}_i\|^2 - - (\nabla \mathbf{m_1}_i \cdot \nabla \mathbf{m_2}_i )^2 - = \|\phi_{cx}\|^2 + \|\phi_{cy}\|^2 + \|\phi_{cz}\|^2 - - (optional strategy, not used in this script) + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + Returns + ------- + float + The regularization function evaluated for the model provided. """ + m1, m2 = self.wire_map * model Av = self._Av G = self._G @@ -145,14 +245,17 @@ def __call__(self, model): ) def deriv(self, model): - """ - Computes the Jacobian of the cross-gradient. - - :param list of numpy.ndarray ind_models: [model1, model2,...] + """Jacobian of the regularization function evaluated for the model provided. - :rtype: numpy.ndarray - :return: result: gradient of the cross-gradient with respect to model1, model2 + Parameters + ---------- + model : list of (n_param, ) numpy.ndarray + The models for which the gradient is evaluated. + Returns + ------- + (n_param, ) numpy.ndarray + Jacobian of the regularization function evaluated for the model provided. """ m1, m2 = self.wire_map * model @@ -169,17 +272,21 @@ def deriv(self, model): ] def deriv2(self, model, v=None): - """ - Computes the Hessian of the cross-gradient. - - :param list of numpy.ndarray ind_models: [model1, model2, ...] - :param numpy.ndarray v: vector to be multiplied by Hessian - - :rtype: scipy.sparse.csr_matrix if v is None - numpy.ndarray if v is not None - :return Hessian matrix if v is None - Hessian multiplied by vector if v is not No + """Hessian of the regularization function evaluated for the model provided. + + Parameters + ---------- + model : list of (n_param, ) numpy.ndarray + The models for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian + for the models provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. """ m1, m2 = self.wire_map * model diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index 8766be65eb..5f4127f030 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -12,12 +12,115 @@ class JointTotalVariation(BaseSimilarityMeasure): - r""" - The joint total variation constraint for joint inversions. + r"""Joint total variation regularization for joint inversion. + + ``JointTotalVariation`` regularization aims to ensure non-zero gradients in the recovered + model to occur at the same locations for all physical property distributions. + It assumes structures within each physical property distribution are sparse and + correlated with one another. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : SimPEG.maps.WireMap + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + eps : float + Needs documentation!!! + + Notes + ----- + Consider the case where the model is comprised of two physical properties + :math:`m_1` and :math:`m_2`. Here, we define the regularization + function for joint total variation as + (`Haber and Gazit, 2013 `__): + + .. math:: + \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \Big [ \, \big | \nabla m_1 \big |^2 \, + \, \big | \nabla m_2 \big |^2 \, \Big ]^{1/2} \, dv + + where :math:`w(r)` is a user-defined weighting function. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discretized approximation for the regularization + function (objective function) is given by: + + .. math:: + \gamma (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ \, + \Big | (\nabla m_1)_i \Big |^2 \, + \, \Big | (\nabla m_2)_i \Big |^2 \, \bigg ]^{1/2} + + where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. + + In practice, we define the model :math:`\mathbf{m}` as a discrete + vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + where :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}` are the discrete representations + of the respective physical properties on the mesh. The discrete regularization function + is therefore equivalent to an objective function of the form: + + .. math:: + \gamma (\mathbf{m}) = \frac{1}{2} \, \mathbf{e}^T \Bigg ( \, + \mathbf{W \, A} \bigg [ \sum_k (\mathbf{G \, m_k})^2 \bigg ] \; + \; \epsilon \mathbf{v}^2 + \, \Bigg )^{1/2} + + where exponents are computed elementwise, + + - :math:`\mathbf{e}` is a vector of 1s, + - :math:`\mathbf{W}` is the weighting matrix for joint total variation regularization, + - :math:`\mathbf{A}` averages vectors from faces to cell centers, + - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), + - :math:`\mathbf{v}` are the cell volumes, and + - :math:`\epsilon` is a constant added for continuous differentiability (set with the `eps` property), + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. The weighting applied within the objective function is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply weights within the regularization is given by: + + .. math:: + \boldsymbol{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^2 \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = JointTotalVariation( + >>> mesh, wire_map, weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') - ..math :: - \phi_sim(\mathbf{m_1},\mathbf{m_2}) = \lambda \sum_{i=1}^{M} V_i\sqrt{| - \nabla \mathbf{m_1}_i|^2 +|\nabla \mathbf{m_2}_i|^2} """ def __init__(self, mesh, wire_map, eps=1e-8, **kwargs): @@ -29,8 +132,16 @@ def __init__(self, mesh, wire_map, eps=1e-8, **kwargs): @property def W(self): - """ - Weighting matrix + r"""Weighting matrix for joint total variation regularization. + + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`JointTotalVariation` + regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the regularization. """ if getattr(self, "_W", None) is None: weights = np.prod(list(self._weights.values()), axis=0) @@ -41,6 +152,7 @@ def W(self): @property def wire_map(self): + # Docs inherited from BaseSimilarityMeasure return self._wire_map @wire_map.setter @@ -57,18 +169,20 @@ def wire_map(self, wires): self._wire_map = wires def __call__(self, model): - """ - Computes the sum of all joint total variation values. + """Evaluate the joint total variation regularization function for the model provided. + + See the *Notes* section of the documentation for the :class:`JointTotalVariation` class + for a full description of the regularization function. Parameters ---------- - model : numpy.ndarray - stacked array of individual models np.r_[model1, model2,...] + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. Returns ------- float - The computed value of the joint total variation term. + The regularization function evaluated for the model provided. """ W = self.W G = self._G @@ -82,18 +196,17 @@ def __call__(self, model): return np.sum(sq) def deriv(self, model): - """ - Computes the derivative of the joint total variation. + """Jacobian of the regularization function evaluated for the model provided. Parameters ---------- - model : numpy.ndarray - stacked array of individual models np.r_[model1, model2,...] + model : list of (n_param, ) numpy.ndarray + The models for which the gradient is evaluated. Returns ------- - numpy.ndarray - The gradient of joint total variation with respect to the model + (n_param, ) numpy.ndarray + Jacobian of the regularization function evaluated for the model provided. """ W = self.W G = self._G @@ -113,8 +226,7 @@ def deriv(self, model): return np.concatenate(ps) def deriv2(self, model, v=None): - """ - Computes the Hessian of the joint total variation. + """Hessian of the regularization function evaluated for the model provided. Parameters ---------- @@ -126,6 +238,7 @@ def deriv2(self, model, v=None): Returns ------- numpy.ndarray or scipy.sparse.csr_matrix + Hessian of the regularization function evaluated for the model provided. The Hessian of joint total variation with respect to the model times a vector or the full Hessian if `v` is `None`. """ diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 21788238f0..90d24acea1 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -15,9 +15,14 @@ class RegularizationMesh(props.BaseSimPEG): """Regularization Mesh - The ``RegularizationMesh`` class is used to construct discrete operators for - the objective function(s) defining the regularization. Discrete operators - only act on active cells in the inversion, thus reducing computational cost. + The ``RegularizationMesh`` class is used to construct differencing and averaging operators + for the objective function(s) defining the regularization. In practice, these operators are + not constructed by creating instances of ``RegularizationMesh``. The operators are instead + constructed (and sometimes stored) when called as a property of the mesh. + The ``RegularizationMesh`` class is built using much of the functionality for the + (link to discretize DiffOperators) class. + However, operators constructed using the ``RegularizationMesh`` class have been modified to + act only on interior faces and active cells in the inversion, thus reducing computational cost. Parameters ---------- @@ -151,8 +156,8 @@ def Pafx(self) -> sp.csr_matrix: Returns ------- - (n_faces_x, n_active_x) scipy.sparse.csr_matrix - Projection matrix from active x-faces to all x-faces in the mesh + (n_faces_x, n_active_faces_x) scipy.sparse.csr_matrix + Projection matrix from active x-faces to all x-faces in the mesh. """ if getattr(self, "_Pafx", None) is None: if self.mesh._meshType == "TREE": @@ -173,9 +178,12 @@ def Pafx(self) -> sp.csr_matrix: @property def Pafy(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active y-faces - to full modelling space (ie. nFy x nactive_cells_Fy ). + """Projection matrix from active y-faces to all y-faces in the mesh. + + Returns + ------- + (n_faces_y, n_active_faces_y) scipy.sparse.csr_matrix + Projection matrix from active y-faces to all y-faces in the mesh. """ if getattr(self, "_Pafy", None) is None: if self.mesh._meshType == "TREE": @@ -198,9 +206,12 @@ def Pafy(self) -> sp.csr_matrix: @property def Pafz(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active z-faces - to full modelling space (ie. nFz x nactive_cells_Fz ). + """Projection matrix from active z-faces to all z-faces in the mesh. + + Returns + ------- + (n_faces_z, n_active_faces_z) scipy.sparse.csr_matrix + Projection matrix from active z-faces to all z-faces in the mesh. """ if getattr(self, "_Pafz", None) is None: if self.mesh._meshType == "TREE": @@ -221,9 +232,14 @@ def Pafz(self) -> sp.csr_matrix: @property def average_face_to_cell(self) -> sp.csr_matrix: - """ - Vertically stacked matrix of cell averaging operators from active - cell centers to active faces along each dimension of the mesh. + """Averaging operator from faces to cell centers. + + Built from (link to discretize DiffOperators average face to cell). + + Returns + ------- + (n_cells, n_faces) scipy.sparse.csr_matrix + Averaging operator from faces to cell centers. """ if self.dim == 1: return self.aveFx2CC @@ -234,8 +250,16 @@ def average_face_to_cell(self) -> sp.csr_matrix: @property def aveFx2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active x-faces. + """Averaging operator from active cell centers to active x-faces. + + Modified from the transpose of + (link to discretize DiffOperators method); + an operator that projects from all x-faces to all cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_x) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active x-faces. """ if getattr(self, "_aveFx2CC", None) is None: if self.mesh._meshType == "TREE": @@ -249,8 +273,16 @@ def aveFx2CC(self) -> sp.csr_matrix: @property def aveCC2Fx(self) -> sp.csr_matrix: - """ - Averaging from active x-faces to active cell centers. + """Averaging operator from active x-faces to active cell centers. + + Modified from + (link to discretize DiffOperators method); + an operator that projects from all x-faces to all cell centers. + + Returns + ------- + (n_active_faces_x, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active x-faces to active cell centers. """ if getattr(self, "_aveCC2Fx", None) is None: if self.mesh._meshType == "TREE": @@ -265,8 +297,16 @@ def aveCC2Fx(self) -> sp.csr_matrix: @property def aveFy2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active y-faces. + """Averaging operator from active cell centers to active y-faces. + + Modified from the transpose of + (link to discretize DiffOperators method); + an operator that projects from y-faces to cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_y) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active y-faces. """ if getattr(self, "_aveFy2CC", None) is None: if self.mesh._meshType == "TREE": @@ -282,8 +322,16 @@ def aveFy2CC(self) -> sp.csr_matrix: @property def aveCC2Fy(self) -> sp.csr_matrix: - """ - Averaging matrix from active y-faces to active cell centers. + """Averaging operator from active y-faces to active cell centers. + + Modified from + (link to discretize DiffOperators method); + an operator that projects from all y-faces to all cell centers. + + Returns + ------- + (n_active_faces_y, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active y-faces to active cell centers. """ if getattr(self, "_aveCC2Fy", None) is None: if self.mesh._meshType == "TREE": @@ -300,8 +348,16 @@ def aveCC2Fy(self) -> sp.csr_matrix: @property def aveFz2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active z-faces. + """Averaging operator from active cell centers to active z-faces. + + Modified from the transpose of + (link to discretize DiffOperators method); + an operator that projects from z-faces to cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_z) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active z-faces. """ if getattr(self, "_aveFz2CC", None) is None: if self.mesh._meshType == "TREE": @@ -315,8 +371,16 @@ def aveFz2CC(self) -> sp.csr_matrix: @property def aveCC2Fz(self) -> sp.csr_matrix: - """ - Averaging matrix from active z-faces to active cell centers. + """Averaging operator from active z-faces to active cell centers. + + Modified from + (link to discretize DiffOperators method); + an operator that projects from all z-faces to all cell centers. + + Returns + ------- + (n_active_faces_z, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active z-faces to active cell centers. """ if getattr(self, "_aveCC2Fz", None) is None: if self.mesh._meshType == "TREE": @@ -331,16 +395,27 @@ def aveCC2Fz(self) -> sp.csr_matrix: @property def base_length(self) -> float: - """The smallest core cell size.""" + """Smallest dimension (i.e. edge length) for smallest cell in the mesh. + + Returns + ------- + float + Smallest dimension (i.e. edge length) for smallest cell in the mesh. + """ if getattr(self, "_base_length", None) is None: self._base_length = self.mesh.edge_lengths.min() return self._base_length @property def cell_gradient(self) -> sp.csr_matrix: - """ - Vertically stacked matrix of cell gradients along each dimension of - the mesh. + """Cell gradient operator (cell centers to faces). + + Built from (link to discretize DiffOperators method). + + Returns + ------- + (n_faces, n_cells) scipy.sparse.csr_matrix + Cell gradient operator (cell centers to faces). """ if self.dim == 1: return self.cell_gradient_x @@ -353,8 +428,16 @@ def cell_gradient(self) -> sp.csr_matrix: @property def cell_gradient_x(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the x-direction. + """Cell-centered x-derivative operator on active cells. + + Cell centered x-derivative operator that maps from active cells + to active x-faces. Modified from + (link to discretize DiffOperators method) + + Returns + ------- + (n_active_faces_x, n_active_cells) scipy.sparse.csr_matrix + Cell-centered x-derivative operator on active cells. """ if getattr(self, "_cell_gradient_x", None) is None: if self.mesh._meshType == "TREE": @@ -375,8 +458,16 @@ def cell_gradient_x(self) -> sp.csr_matrix: @property def cell_gradient_y(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the y-direction. + """Cell-centered y-derivative operator on active cells. + + Cell centered y-derivative operator that maps from active cells + to active y-faces. Modified from + (link to discretize DiffOperators method) + + Returns + ------- + (n_active_faces_y, n_active_cells) scipy.sparse.csr_matrix + Cell-centered y-derivative operator on active cells. """ if getattr(self, "_cell_gradient_y", None) is None: if self.mesh._meshType == "TREE": @@ -397,8 +488,16 @@ def cell_gradient_y(self) -> sp.csr_matrix: @property def cell_gradient_z(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the z-direction. + """Cell-centered z-derivative operator on active cells. + + Cell centered z-derivative operator that maps from active cells + to active z-faces. Modified from + (link to discretize DiffOperators method) + + Returns + ------- + (n_active_faces_z, n_active_cells) scipy.sparse.csr_matrix + Cell-centered z-derivative operator on active cells. """ if getattr(self, "_cell_gradient_z", None) is None: if self.mesh._meshType == "TREE": @@ -444,8 +543,12 @@ def cell_gradient_z(self) -> sp.csr_matrix: @property def cell_distances_x(self) -> np.ndarray: - """ - Cell center distance array along the x-direction. + """Cell center distance array along the x-direction. + + Returns + ------- + (n_active_faces_x, ) numpy.ndarray + Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: Ave = self.aveCC2Fx @@ -454,8 +557,12 @@ def cell_distances_x(self) -> np.ndarray: @property def cell_distances_y(self) -> np.ndarray: - """ - Cell center distance array along the y-direction. + """Cell center distance array along the y-direction. + + Returns + ------- + (n_active_faces_y, ) numpy.ndarray + Cell center distance array along the y-direction. """ if getattr(self, "_cell_distances_y", None) is None: Ave = self.aveCC2Fy @@ -464,8 +571,12 @@ def cell_distances_y(self) -> np.ndarray: @property def cell_distances_z(self) -> np.ndarray: - """ - Cell center distance array along the z-direction. + """Cell center distance array along the z-direction. + + Returns + ------- + (n_active_faces_z, ) numpy.ndarray + Cell center distance array along the z-direction. """ if getattr(self, "_cell_distances_z", None) is None: Ave = self.aveCC2Fz From e7367ff9e978b8839caf164337379968e896210c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 1 Jun 2023 13:00:09 -0700 Subject: [PATCH 118/455] another closing thread tests --- SimPEG/meta/multiprocessing.py | 66 +++++++++++++------------- tests/meta/test_multiprocessing_sim.py | 6 +-- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 2b1d66c0b5..b024c8225e 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -71,36 +71,39 @@ def run(self): (key,) = args _cached_items.pop(key, None) else: - if op == 0: - # store_model - (m,) = args - sim.model = m - elif op == 1: - # create fields - f_key = uuid.uuid4().hex - r_queue.put(f_key) - fields = sim.fields(sim.model) - _cached_items[f_key] = fields - elif op == 2: - # do dpred - (f_key,) = args - fields = _cached_items[f_key] - r_queue.put(sim.dpred(sim.model, fields)) - elif op == 3: - # do jvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jvec(sim.model, v, fields)) - elif op == 4: - # do jtvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jtvec(sim.model, v, fields)) - elif op == 5: - # do jtj_diag - w, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.getJtJdiag(sim.model, w, fields)) + try: + if op == 0: + # store_model + (m,) = args + sim.model = m + elif op == 1: + # create fields + f_key = uuid.uuid4().hex + r_queue.put(f_key) + fields = sim.fields(sim.model) + _cached_items[f_key] = fields + elif op == 2: + # do dpred + (f_key,) = args + fields = _cached_items[f_key] + r_queue.put(sim.dpred(sim.model, fields)) + elif op == 3: + # do jvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jvec(sim.model, v, fields)) + elif op == 4: + # do jtvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jtvec(sim.model, v, fields)) + elif op == 5: + # do jtj_diag + w, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.getJtJdiag(sim.model, w, fields)) + except Exception as err: + r_queue.put(err) def store_model(self, m): self._check_closed() @@ -271,12 +274,11 @@ def getJtJdiag(self, m, W=None, f=None): self._jtjdiag = jtj_diag return self._jtjdiag - def close(self): + def join(self): for p in self._sim_processes: if p.is_alive(): p.task_queue.put(None) p.join() - p.close() class MultiprocessingSumMetaSimulation( diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 52712bac61..5f393c7b73 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -101,7 +101,7 @@ def test_meta_correctness(): except Exception as err: raise err finally: - parallel_sim.close() + parallel_sim.join() def test_sum_correctness(): @@ -182,7 +182,7 @@ def test_sum_correctness(): except Exception as err: raise err finally: - parallel_sim.close() + parallel_sim.join() def test_repeat_correctness(): @@ -237,4 +237,4 @@ def test_repeat_correctness(): except Exception as err: raise err finally: - parallel_sim.close() + parallel_sim.join() From 8b1a73520ccdb2b9eb65a73f5949ddb511d89f8e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 1 Jun 2023 16:12:02 -0700 Subject: [PATCH 119/455] Remove unused fixture --- tests/base/test_objective_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index b4449fa4f0..2bc0207531 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -354,7 +354,7 @@ def test_add(self, unpack_on_add): combo_1 = combo_2.objfcts[1] assert combo_1.multipliers == [2, 3] - def test_add_multiple_terms(self, unpack_on_add): + def test_add_multiple_terms(self): """Test addition of multiple BaseObjectiveFunctions""" n_params = 10 phi1 = objective_function.L2ObjectiveFunction(nP=n_params) From 2c65d67e655fb2df9429be372dc7e720291119d9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 1 Jun 2023 16:12:36 -0700 Subject: [PATCH 120/455] Update tests Stick with the way combo multiplications are handled. --- tests/base/test_objective_function.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 2bc0207531..cab7ea555d 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -367,7 +367,12 @@ def test_add_multiple_terms(self): @pytest.mark.parametrize("unpack_on_add", (True, False)) def test_add_and_mul(self, unpack_on_add): - """Test ComboObjectiveFunction addition with multiplication""" + """ + Test ComboObjectiveFunction addition with multiplication + + After multiplying a Combo with a scalar, the `__mul__` method creates + another Combo for it. + """ n_params = 10 phi1 = objective_function.L2ObjectiveFunction(nP=n_params) phi2 = objective_function.L2ObjectiveFunction(nP=n_params) @@ -376,16 +381,9 @@ def test_add_and_mul(self, unpack_on_add): [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add ) combo_2 = 5 * phi3 + 1.2 * combo_1 - if unpack_on_add: - assert len(combo_2) == 3 - assert combo_2.multipliers == [5, 1.2 * 2, 1.2 * 3] - assert combo_2.objfcts == [phi3, phi1, phi2] - else: - assert len(combo_2) == 2 - assert combo_2.multipliers == [5, 1] - assert combo_2.objfcts == [phi3, combo_1] - combo_1 = combo_2.objfcts[1] - assert combo_1.multipliers == [1.2 * 2, 1.2 * 3] + assert len(combo_2) == 2 + assert combo_2.multipliers == [5, 1.2] + assert combo_2.objfcts == [phi3, combo_1] if __name__ == "__main__": From c6361677b655e999097272384d321517974315cd Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 2 Jun 2023 11:18:15 -0700 Subject: [PATCH 121/455] wrap entire inner loop in a try except block --- SimPEG/meta/multiprocessing.py | 80 ++++++++++++++++------------------ 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index b024c8225e..ac772116fa 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -61,49 +61,45 @@ def run(self): # None is a poison pill message to kill this loop. break op, args = task - if op == "get_item": - (key,) = args - try: + try: + if op == "get_item": + (key,) = args r_queue.put(_cached_items[key]) - except Exception as err: - r_queue.put(err) - elif op == "del_item": - (key,) = args - _cached_items.pop(key, None) - else: - try: - if op == 0: - # store_model - (m,) = args - sim.model = m - elif op == 1: - # create fields - f_key = uuid.uuid4().hex - r_queue.put(f_key) - fields = sim.fields(sim.model) - _cached_items[f_key] = fields - elif op == 2: - # do dpred - (f_key,) = args - fields = _cached_items[f_key] - r_queue.put(sim.dpred(sim.model, fields)) - elif op == 3: - # do jvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jvec(sim.model, v, fields)) - elif op == 4: - # do jtvec - v, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.Jtvec(sim.model, v, fields)) - elif op == 5: - # do jtj_diag - w, f_key = args - fields = _cached_items[f_key] - r_queue.put(sim.getJtJdiag(sim.model, w, fields)) - except Exception as err: - r_queue.put(err) + elif op == "del_item": + (key,) = args + _cached_items.pop(key, None) + elif op == 0: + # store_model + (m,) = args + sim.model = m + elif op == 1: + # create fields + f_key = uuid.uuid4().hex + r_queue.put(f_key) + fields = sim.fields(sim.model) + _cached_items[f_key] = fields + elif op == 2: + # do dpred + (f_key,) = args + fields = _cached_items[f_key] + r_queue.put(sim.dpred(sim.model, fields)) + elif op == 3: + # do jvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jvec(sim.model, v, fields)) + elif op == 4: + # do jtvec + v, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.Jtvec(sim.model, v, fields)) + elif op == 5: + # do jtj_diag + w, f_key = args + fields = _cached_items[f_key] + r_queue.put(sim.getJtJdiag(sim.model, w, fields)) + except Exception as err: + r_queue.put(err) def store_model(self, m): self._check_closed() From 51c7c725b72b65b2df790184054b42638b051ce8 Mon Sep 17 00:00:00 2001 From: dccowan Date: Sat, 3 Jun 2023 09:41:24 -0700 Subject: [PATCH 122/455] updates from PR comments --- SimPEG/regularization/__init__.py | 4 +- SimPEG/regularization/base.py | 103 ++++++++++--------- SimPEG/regularization/cross_gradient.py | 13 +-- SimPEG/regularization/jtv.py | 10 +- SimPEG/regularization/regularization_mesh.py | 26 ++--- SimPEG/regularization/sparse.py | 42 ++++---- 6 files changed, 103 insertions(+), 95 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 79bfb59522..815e4adf88 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -33,10 +33,10 @@ The global objective function contains two terms: a data misfit term :math:`\phi_d` which ensures data predicted by the recovered model adequately reproduces the observed data, and the model objective function :math:`\phi_m` which is comprised of one or more -regularization functions :math:`\gamma_i (m)`. I.e.: +regularization functions (objective functions) :math:`\phi_i (m)`. I.e.: .. math:: - \phi_m (m) = \sum_i \alpha_i \, \gamma_i (m) + \phi_m (m) = \sum_i \alpha_i \, \phi_i (m) The model objective function imposes all of the desired constraints on the recovered model. Constants :math:`\alpha_i` weight the relative contributions of the regularization diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index bb9ff365f4..3607793b10 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -31,7 +31,7 @@ class BaseRegularization(BaseObjectiveFunction): cells that are active in the inversion. If ``None``, all cells are active. mapping : None, SimPEG.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. + If ``None``, the mapping is set to :obj:`SimPEG.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray Reference model. If ``None``, the reference model in the inversion is set to the starting model. @@ -39,8 +39,8 @@ class BaseRegularization(BaseObjectiveFunction): Units for the model parameters. Some regularization classes behave differently depending on the units; e.g. 'radian'. weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) - numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + Weight multipliers to customize the least-squares function. + Each value is a numpy.ndarray of shape(:py:property:`~.regularization.RegularizationMesh.n_cells`, ). """ @@ -287,6 +287,11 @@ def cell_weights(self, value): def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. + Parameters + ------------ + key: str + Name of the weights requested. + Returns ------- (n_cells, ) numpy.ndarray @@ -428,11 +433,11 @@ def f_m_deriv(self, m) -> csr_matrix: def deriv(self, m) -> np.ndarray: r"""Jacobian of the regularization function evaluated for the model provided. - Where :math:`\gamma (\mathbf{m})` is the discrete regularization function, + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), this method returns the derivative (Jacobian) with respect to the model parameters: .. math:: - \frac{\partial \gamma}{\partial \mathbf{m}} \, \bigg |_\mathbf{m} + \frac{\partial \phi}{\partial \mathbf{m}} \, \bigg |_\mathbf{m} evaluated at the model :math:`\mathbf{m}` provided. @@ -453,16 +458,16 @@ def deriv(self, m) -> np.ndarray: def deriv2(self, m, v=None) -> csr_matrix: r"""Hessian of the regularization function evaluated for the model provided. - Where :math:`\gamma (\mathbf{m})` is the discrete regularization function, + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), this method returns the second derivative (Hessian) with respect to the model parameters: .. math:: - \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} or the second-derivative (Hessian) multiplied by a given vector :math:`(\mathbf{v})` .. math:: - \bigg [ \frac{\partial^2 \gamma}{\partial \mathbf{m}^2} + \bigg [ \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} \bigg ] \mathbf{v} @@ -528,7 +533,7 @@ class Smallness(BaseRegularization): We define the regularization function for smallness as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` @@ -539,7 +544,7 @@ class Smallness(BaseRegularization): function (objective function) is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and @@ -548,7 +553,7 @@ class Smallness(BaseRegularization): This is equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where @@ -593,9 +598,9 @@ def __init__(self, mesh, **kwargs): self.set_weights(volume=self.regularization_mesh.vol) def f_m(self, m) -> np.ndarray: - r"""Evaluate least-squares regularization kernel. + r"""Evaluate the regularization kernel function. - For smallness regularization, the least-squares regularization kernel is given by: + For smallness regularization, the regularization kernel function is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -611,7 +616,7 @@ def f_m(self, m) -> np.ndarray: Returns ------- numpy.ndarray - The least-squares regularization kernel. + The regularization kernel function evaluated for the model provided. Notes ----- @@ -625,7 +630,7 @@ def f_m(self, m) -> np.ndarray: :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is the weighting matrix. See the :class:`Smallness` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -639,10 +644,10 @@ def f_m(self, m) -> np.ndarray: return self.mapping * self._delta_m(m) def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the least-squares regularization kernel. + r"""Derivative of the regularization kernel function. - For ``Smallness`` regularization, the derivative of the least-squares regularization - kernel with respect to the model is given by: + For ``Smallness`` regularization, the derivative of the regularization kernel function + with respect to the model is given by: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} @@ -657,7 +662,7 @@ def f_m_deriv(self, m) -> csr_matrix: Returns ------- scipy.sparse.csr_matrix - The derivative of the least-squares regularization kernel. + The derivative of the regularization kernel function. Notes ----- @@ -671,7 +676,7 @@ def f_m_deriv(self, m) -> csr_matrix: :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is the weighting matrix. See the :class:`Smallness` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} @@ -734,10 +739,11 @@ class SmoothnessFirstOrder(BaseRegularization): Notes ----- - We define the regularization function for first-order smoothness along the x-direction as: + We define the regularization function (objective function) for first-order smoothness + along the x-direction as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. @@ -747,7 +753,7 @@ class SmoothnessFirstOrder(BaseRegularization): function (objective function) is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh @@ -756,7 +762,7 @@ class SmoothnessFirstOrder(BaseRegularization): This is equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, G_x m } \, \Big \|^2 + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, G_x m } \, \Big \|^2 where @@ -773,7 +779,7 @@ class SmoothnessFirstOrder(BaseRegularization): In this case, the objective function becomes: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting a reference model with the @@ -902,10 +908,10 @@ def _multiplier_pair(self): return f"alpha_{self.orientation}" def f_m(self, m): - r"""Evaluate least-squares regularization kernel. + r"""Evaluate the regularization kernel function. For first-order smoothness regularization in the x-direction, - the least-squares regularization kernel is given by: + the regularization kernel function is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -923,7 +929,7 @@ def f_m(self, m): Returns ------- numpy.ndarray - The least-squares regularization kernel. + The regularization kernel function. Notes ----- @@ -940,7 +946,7 @@ def f_m(self, m): the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -960,10 +966,10 @@ def f_m(self, m): return dfm_dl def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the least-squares regularization kernel. + r"""Derivative of the regularization kernel function. For first-order smoothness regularization in the x-direction, the derivative of the - least-squares regularization kernel with respect to the model is given by: + regularization kernel function with respect to the model is given by: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} @@ -979,7 +985,7 @@ def f_m_deriv(self, m) -> csr_matrix: Returns ------- scipy.sparse.csr_matrix - The derivative of the least-squares regularization kernel. + The derivative of the regularization kernel function. Notes ----- @@ -996,7 +1002,7 @@ def f_m_deriv(self, m) -> csr_matrix: the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1098,10 +1104,11 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): Notes ----- - We define the regularization function for second-order smoothness along the x-direction as: + We define the regularization function (objective function) for second-order + smoothness along the x-direction as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. @@ -1111,7 +1118,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): function (objective function) is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the @@ -1120,7 +1127,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): This is equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 + \phi (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 where @@ -1134,7 +1141,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): In this case, the objective function becomes: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting a reference model with the @@ -1169,10 +1176,10 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): """ def f_m(self, m): - r"""Evaluate least-squares regularization kernel. + r"""Evaluate the regularization kernel function. For second-order smoothness regularization in the x-direction, - the least-squares regularization kernel is given by: + the regularization kernel function is given by: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1189,7 +1196,7 @@ def f_m(self, m): Returns ------- numpy.ndarray - The least-squares regularization kernel. + The regularization kernel function. Notes ----- @@ -1206,7 +1213,7 @@ def f_m(self, m): the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1229,10 +1236,10 @@ def f_m(self, m): return dfm_dl2 def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the least-squares regularization kernel. + r"""Derivative of the regularization kernel function. - For second-order smoothness regularization, the derivative of the least-squares - regularization kernel with respect to the model is given by: + For second-order smoothness regularization, the derivative of the + regularization kernel function with respect to the model is given by: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} @@ -1247,7 +1254,7 @@ def f_m_deriv(self, m) -> csr_matrix: Returns ------- scipy.sparse.csr_matrix - The derivative of the least-squares regularization kernel. + The derivative of the regularization kernel function. Notes ----- @@ -1264,7 +1271,7 @@ def f_m_deriv(self, m) -> csr_matrix: the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. - We define the least-squares regularization kernel :math:`\mathbf{f_m}` as: + We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -1274,7 +1281,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 - The derivate of the regularization kernel with respect to the model is: + The derivate of the regularization kernel function with respect to the model is: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index 90740e5ffb..05677884c6 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -29,7 +29,7 @@ class CrossGradient(BaseSimilarityMeasure): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - wire_map : SimPEG.maps.WireMap + wire_map : SimPEG.maps.Wires Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh`` to the entire model. reference_model : None, (n_param, ) numpy.ndarray @@ -48,10 +48,11 @@ class CrossGradient(BaseSimilarityMeasure): ----- Consider the case where the model is comprised of two physical properties :math:`m_1` and :math:`m_2`. Here, we define the regularization - function for cross-gradient as (`Haber and Gazit, 2013 `__): + function (objective function) for cross-gradient as + (`Haber and Gazit, 2013 `__): .. math:: - \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big | \nabla m_1 \, \times \, \nabla m_2 \, \Big |^2 \, dv where :math:`w(r)` is a user-defined weighting function. @@ -59,7 +60,7 @@ class CrossGradient(BaseSimilarityMeasure): the regularization function can be re-expressed as: .. math:: - \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, + \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv @@ -68,7 +69,7 @@ class CrossGradient(BaseSimilarityMeasure): function (objective function) is given by: .. math:: - \gamma (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ + \phi (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] @@ -87,7 +88,7 @@ class CrossGradient(BaseSimilarityMeasure): is therefore equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = + \phi (\mathbf{m}) = \frac{1}{2} \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T \Big [ \mathbf{W A} \big ( \mathbf{G \, m_2} \big )^2 \Big ] - \frac{1}{2} \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index 5f4127f030..b546e997fd 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -27,7 +27,7 @@ class JointTotalVariation(BaseSimilarityMeasure): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - wire_map : SimPEG.maps.WireMap + wire_map : SimPEG.maps.Wires Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh`` to the entire model. reference_model : None, (n_param, ) numpy.ndarray @@ -46,11 +46,11 @@ class JointTotalVariation(BaseSimilarityMeasure): ----- Consider the case where the model is comprised of two physical properties :math:`m_1` and :math:`m_2`. Here, we define the regularization - function for joint total variation as + function (objective function) for joint total variation as (`Haber and Gazit, 2013 `__): .. math:: - \gamma (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, \big | \nabla m_1 \big |^2 \, + \, \big | \nabla m_2 \big |^2 \, \Big ]^{1/2} \, dv where :math:`w(r)` is a user-defined weighting function. @@ -60,7 +60,7 @@ class JointTotalVariation(BaseSimilarityMeasure): function (objective function) is given by: .. math:: - \gamma (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ \, + \phi (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ \, \Big | (\nabla m_1)_i \Big |^2 \, + \, \Big | (\nabla m_2)_i \Big |^2 \, \bigg ]^{1/2} where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and @@ -78,7 +78,7 @@ class JointTotalVariation(BaseSimilarityMeasure): is therefore equivalent to an objective function of the form: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \, \mathbf{e}^T \Bigg ( \, + \phi (\mathbf{m}) = \frac{1}{2} \, \mathbf{e}^T \Bigg ( \, \mathbf{W \, A} \bigg [ \sum_k (\mathbf{G \, m_k})^2 \bigg ] \; + \; \epsilon \mathbf{v}^2 \, \Bigg )^{1/2} diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 90d24acea1..c2724aef3f 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -19,8 +19,8 @@ class RegularizationMesh(props.BaseSimPEG): for the objective function(s) defining the regularization. In practice, these operators are not constructed by creating instances of ``RegularizationMesh``. The operators are instead constructed (and sometimes stored) when called as a property of the mesh. - The ``RegularizationMesh`` class is built using much of the functionality for the - (link to discretize DiffOperators) class. + The ``RegularizationMesh`` class is built using much of the functionality from the + :py:class:`discretize.operators.differential_operators.DiffOperators` class. However, operators constructed using the ``RegularizationMesh`` class have been modified to act only on interior faces and active cells in the inversion, thus reducing computational cost. @@ -234,7 +234,7 @@ def Pafz(self) -> sp.csr_matrix: def average_face_to_cell(self) -> sp.csr_matrix: """Averaging operator from faces to cell centers. - Built from (link to discretize DiffOperators average face to cell). + Built from :py:attr:`discretize.operators.differential_operators.DiffOperators.average_face_to_cell`. Returns ------- @@ -253,7 +253,7 @@ def aveFx2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active x-faces. Modified from the transpose of - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFx2CC`; an operator that projects from all x-faces to all cell centers. Returns @@ -276,7 +276,7 @@ def aveCC2Fx(self) -> sp.csr_matrix: """Averaging operator from active x-faces to active cell centers. Modified from - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fx`; an operator that projects from all x-faces to all cell centers. Returns @@ -300,7 +300,7 @@ def aveFy2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active y-faces. Modified from the transpose of - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFy2CC`; an operator that projects from y-faces to cell centers. Returns @@ -325,7 +325,7 @@ def aveCC2Fy(self) -> sp.csr_matrix: """Averaging operator from active y-faces to active cell centers. Modified from - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fy`; an operator that projects from all y-faces to all cell centers. Returns @@ -351,7 +351,7 @@ def aveFz2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active z-faces. Modified from the transpose of - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFz2CC`; an operator that projects from z-faces to cell centers. Returns @@ -374,7 +374,7 @@ def aveCC2Fz(self) -> sp.csr_matrix: """Averaging operator from active z-faces to active cell centers. Modified from - (link to discretize DiffOperators method); + :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fz`; an operator that projects from all z-faces to all cell centers. Returns @@ -410,7 +410,7 @@ def base_length(self) -> float: def cell_gradient(self) -> sp.csr_matrix: """Cell gradient operator (cell centers to faces). - Built from (link to discretize DiffOperators method). + Built from :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient`. Returns ------- @@ -432,7 +432,7 @@ def cell_gradient_x(self) -> sp.csr_matrix: Cell centered x-derivative operator that maps from active cells to active x-faces. Modified from - (link to discretize DiffOperators method) + :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_x`. Returns ------- @@ -462,7 +462,7 @@ def cell_gradient_y(self) -> sp.csr_matrix: Cell centered y-derivative operator that maps from active cells to active y-faces. Modified from - (link to discretize DiffOperators method) + :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_y`. Returns ------- @@ -492,7 +492,7 @@ def cell_gradient_z(self) -> sp.csr_matrix: Cell centered z-derivative operator that maps from active cells to active z-faces. Modified from - (link to discretize DiffOperators method) + :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_z`. Returns ------- diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index b155859635..74daca57e6 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -137,7 +137,7 @@ def norm(self, value: float | np.ndarray | None): def get_lp_weights(self, f_m): r"""Compute and return iteratively re-weighted least-squares (IRLS) weights. - For a least-squares regularization kernel function :math:`\mathbf{f_m}(\mathbf{m})` + For a regularization kernel function :math:`\mathbf{f_m}(\mathbf{m})` evaluated at model :math:`\mathbf{m}`, compute and return the IRLS weights. See :py:meth:`Smallness.f_m` and :py:meth:`SmoothnessFirstOrder.f_m` for examples of least-squares regularization kernels. @@ -150,11 +150,11 @@ def get_lp_weights(self, f_m): Parameters ---------- f_m : numpy.ndarray - The least-squares regularization kernel evaluated at the current model. + The regularization kernel function evaluated at the current model. Notes ----- - For a least-squares regularization kernel :math:`\mathbf{f_m}` evaluated at model + For a regularization kernel function :math:`\mathbf{f_m}` evaluated at model :math:`\mathbf{m}`, the IRLS weights are computed via: .. math:: @@ -252,10 +252,10 @@ class SparseSmallness(BaseSparse, Smallness): Notes ----- - We define the regularization function for sparse smallness (compactness) as: + We define the regularization function (objective function) for sparse smallness (compactness) as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, \Big | m(r) - m^{(ref)}(r) \Big |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` @@ -269,7 +269,7 @@ class SparseSmallness(BaseSparse, Smallness): function (objective function) is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_i} where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. @@ -283,7 +283,7 @@ class SparseSmallness(BaseSparse, Smallness): a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: .. math:: - \gamma \big (\mathbf{m}^{(k)} \big ) + \phi \big (\mathbf{m}^{(k)} \big ) = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 @@ -298,7 +298,7 @@ class SparseSmallness(BaseSparse, Smallness): function for IRLS iteration :math:`k` can be expressed as follows: .. math:: - \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, \mathbf{W}^{\! (k)} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where @@ -350,7 +350,7 @@ def update_weights(self, m): Notes ----- - For the model :math:`\mathbf{m}` provided, the least-squares regularization kernel + For the model :math:`\mathbf{m}` provided, the regularization kernel function for sparse smallness is given by: .. math:: @@ -457,11 +457,11 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): Notes ----- - The regularization function for sparse smoothness (blockiness) along the x-direction - as: + The regularization function (objective function) for sparse smoothness (blockiness) + along the x-direction as: .. math:: - \gamma (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, \bigg | \frac{\partial m}{\partial x} \bigg |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`w(r)` @@ -475,7 +475,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): function (objective function) is given by: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \frac{1}{2} \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^{p_i} where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. @@ -489,7 +489,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: .. math:: - \gamma \big (\mathbf{m}^{(k)} \big ) + \phi \big (\mathbf{m}^{(k)} \big ) = \frac{1}{2} \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^{p_i} \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} @@ -506,7 +506,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): function for IRLS iteration :math:`k` can be expressed as follows: .. math:: - \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, \mathbf{W \, R}^{\!\! (k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 where @@ -525,13 +525,13 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): In this case, the objective function becomes: .. math:: - \gamma (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 The least-squares problem for IRLS iteration :math:`k` becomes: .. math:: - \gamma \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, \mathbf{W}^{\! (k)} \mathbf{G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 @@ -598,7 +598,7 @@ def update_weights(self, m): ----- Let us consider the IRLS weights for sparse smoothness along the x-direction. When the class property `gradient_type`=`'components'`, IRLS weights are computed - using the least-squares regularization kernel and we define: + using the regularization kernel function and we define: .. math:: \mathbf{f_m} = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] @@ -607,7 +607,7 @@ def update_weights(self, m): gradient operator along x (i.e. x-derivative), and :math:`\mathbf{m}^{(ref)}` is a reference model (optional, activated using `reference_model_in_smooth`). See :py:meth:`SmoothnessFirstOrder.f_m` for a more comprehensive definition of the - least-squares regularization kernel. + regularization kernel function. However, when the class property `gradient_type`=`'total'`, IRLS weights are computed using the magnitude of the total gradient and we define: @@ -803,8 +803,8 @@ class Sparse(WeightedLeastSquares): .. math:: \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} - where :math:`f_i \in \mathbf{f_m}` define the discrete least-squares regularization kernel - on the mesh. For example, the least-squares regularization kernel for smallness regularization + where :math:`f_i \in \mathbf{f_m}` define the discrete regularization kernel function + on the mesh. For example, the regularization kernel function for smallness regularization is: .. math:: From ac32724a29c044b2c0dc9ebf8bd928011a780b12 Mon Sep 17 00:00:00 2001 From: dccowan Date: Sat, 3 Jun 2023 10:01:07 -0700 Subject: [PATCH 123/455] updates based on PR comments 2. --- SimPEG/regularization/__init__.py | 4 ++-- SimPEG/regularization/base.py | 32 +++++++++++++++---------------- SimPEG/regularization/sparse.py | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 815e4adf88..fe0387cfc3 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -38,7 +38,7 @@ .. math:: \phi_m (m) = \sum_i \alpha_i \, \phi_i (m) -The model objective function imposes all of the desired constraints on the recovered model. +The model objective function imposes all the desired constraints on the recovered model. Constants :math:`\alpha_i` weight the relative contributions of the regularization functions comprising the model objective function. The trade-off parameter :math:`\beta` balances the relative contribution of the data misfit and regularization functions on the @@ -72,7 +72,7 @@ where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradient operators along the x and y-directions, respectively. :math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` are weighting matrices that apply user-defined weights and account for cell dimensions -in the discretization. +in the inversion mesh. The API diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 3607793b10..07c73a7c68 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -311,7 +311,7 @@ def get_weights(self, key) -> np.ndarray: return self._weights[key] def set_weights(self, **weights): - """Adds (or updates) the specified weights to the regularization + """Adds (or updates) the specified weights to the regularization. Parameters ---------- @@ -338,7 +338,7 @@ def set_weights(self, **weights): self._W = None def remove_weights(self, key): - """Removes the weights for the key provided + """Removes the weights for the key provided. Parameters ---------- @@ -363,7 +363,7 @@ def remove_weights(self, key): self._W = None @property - def W(self) -> np.ndarray: + def W(self) -> csr_matrix: r"""Weighting matrix. Returns the weighting matrix for the discrete regularization function. To see how the @@ -541,7 +541,7 @@ class Smallness(BaseRegularization): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is given by: + function (objective function) is expressed in linear form as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \sum_i @@ -605,7 +605,7 @@ def f_m(self, m) -> np.ndarray: .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - where :math:`\mathbf{m}` are the descrete model parameters and :math:`\mathbf{m}^{(ref)}` + where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{m}^{(ref)}` is a reference model. For a more detailed description, see the *Notes* section below. Parameters @@ -686,7 +686,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 - Thus, the derivate with respect to the model is: + Thus, the derivative with respect to the model is: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} @@ -750,7 +750,7 @@ class SmoothnessFirstOrder(BaseRegularization): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is given by: + function (objective function) is expressed in linear form as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \sum_i @@ -917,7 +917,7 @@ def f_m(self, m): \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction - (i.e. x-derivative), :math:`\mathbf{m}` are the descrete model parameters defined on the + (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model parameters defined on the mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). Similarly for smoothness along y and z. @@ -1012,7 +1012,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 - The derivate with respect to the model is therefore: + The derivative with respect to the model is therefore: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} @@ -1020,7 +1020,7 @@ def f_m_deriv(self, m) -> csr_matrix: return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) @property - def W(self): + def W(self) -> csr_matrix: r"""Weighting matrix. Returns the weighting matrix for the objective function. To see how the @@ -1115,7 +1115,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is given by: + function (objective function) is expressed in linear form as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \sum_i @@ -1281,7 +1281,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 - The derivate of the regularization kernel function with respect to the model is: + The derivative of the regularization kernel function with respect to the model is: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} @@ -1293,7 +1293,7 @@ def f_m_deriv(self, m) -> csr_matrix: ) @property - def W(self): + def W(self) -> csr_matrix: r"""Weighting matrix. Returns the weighting matrix for the objective function. To see how the @@ -1445,7 +1445,7 @@ class WeightedLeastSquares(ComboObjectiveFunction): **Alphas and length scales:** The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness - terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as a appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set using the `alpha_x` property. Note that unless the parameters are set manually, second-order smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` @@ -1617,7 +1617,7 @@ def set_weights(self, **weights): Parameters ---------- - **kwargs : key, numpy.ndarray + **weights : key, numpy.ndarray Each keyword argument is added to the weights used by all child regularization objects. They can be accessed with their keyword argument. @@ -1641,7 +1641,7 @@ def remove_weights(self, key): Parameters ---------- key : str - The key for the weights being removed from all child regularization objects. + The name of the weights being removed from all child regularization objects. Examples -------- diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 74daca57e6..2ad8e0c0cc 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -266,7 +266,7 @@ class SparseSmallness(BaseSparse, Smallness): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is given by: + function (objective function) is expressed in linear form as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \sum_i @@ -472,7 +472,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discrete approximation for the regularization - function (objective function) is given by: + function (objective function) is expressed in linear form as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \sum_i From f681f7e978412a9eaed5c23fe4045b31dabbb92c Mon Sep 17 00:00:00 2001 From: dccowan Date: Sat, 3 Jun 2023 15:38:15 -0700 Subject: [PATCH 124/455] small edits and draft of cross reference regularization --- SimPEG/regularization/base.py | 25 +-- SimPEG/regularization/cross_gradient.py | 66 +++++- SimPEG/regularization/jtv.py | 56 ++++- SimPEG/regularization/regularization_mesh.py | 22 +- SimPEG/regularization/vector.py | 219 ++++++++++++++++++- 5 files changed, 338 insertions(+), 50 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 07c73a7c68..7b926102c3 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -422,9 +422,11 @@ def __call__(self, m): return 0.5 * r.dot(r) def f_m(self, m) -> np.ndarray: + """Not implemented for ``BaseRegularization`` class.""" raise AttributeError("Regularization class must have a 'f_m' implementation.") def f_m_deriv(self, m) -> csr_matrix: + """Not implemented for ``BaseRegularization`` class.""" raise AttributeError( "Regularization class must have a 'f_m_deriv' implementation." ) @@ -434,12 +436,11 @@ def deriv(self, m) -> np.ndarray: r"""Jacobian of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method returns the derivative (Jacobian) with respect to the model parameters: + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + I.e.: .. math:: - \frac{\partial \phi}{\partial \mathbf{m}} \, \bigg |_\mathbf{m} - - evaluated at the model :math:`\mathbf{m}` provided. + \frac{\partial \phi}{\partial \mathbf{m}} Parameters ---------- @@ -459,17 +460,15 @@ def deriv2(self, m, v=None) -> csr_matrix: r"""Hessian of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method returns the second derivative (Hessian) with respect to the model parameters: + this method returns the second-derivative (Hessian) with respect to the model parameters: .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \bigg |_\mathbf{m} + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} - or the second-derivative (Hessian) multiplied by a given vector :math:`(\mathbf{v})` + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: .. math:: - \bigg [ \frac{\partial^2 \phi}{\partial \mathbf{m}^2} - \bigg |_\mathbf{m} \bigg ] \mathbf{v} - + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} Parameters ---------- @@ -530,7 +529,7 @@ class Smallness(BaseRegularization): Notes ----- - We define the regularization function for smallness as: + We define the regularization function (objective function) for smallness as: .. math:: \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, @@ -652,7 +651,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix: + where :math:`\mathbf{I}` is the identity matrix. Parameters ---------- @@ -2163,7 +2162,7 @@ def mapping(self, mapping: maps.IdentityMap): # # ############################################################################### class BaseSimilarityMeasure(BaseRegularization): - """Base class for joint inversion regularization. + """Base regularization class for joint inversion. The ``BaseSimilarityMeasure`` class defines properties and methods used by regularization classes for joint inversion. It is not directly used to diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index 05677884c6..56fbf5e583 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -190,7 +190,15 @@ def _calculate_gradient(self, model, normalized=False, rtol=1e-6): return gradient def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): - r"""Calculates the norm of the cross-gradient vectors at cell centers. + r"""Calculates the magnitudes of the cross-gradient vectors at cell centers. + + Computes and returns a discrete approximation to: + + .. math:: + \big | \, \nabla m_1 \, \times \, \nabla m_2 \, \big | + + at all cell centers where :math:`m_1` and :math:`m_2` define the continuous + spacial distribution of physical properties 1 and 2. Parameters ---------- @@ -205,7 +213,7 @@ def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): Returns ------- numpy.ndarray - Calculates the norm of the cross-gradient vectors at cell centers. + Magnitudes of the cross-gradient vectors at cell centers. """ m1, m2 = self.wire_map * model # Compute the gradients and concatenate components. @@ -228,7 +236,7 @@ def __call__(self, model): Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the function is evaluated. + The model; a vector array containing all physical properties. Returns ------- @@ -246,12 +254,26 @@ def __call__(self, model): ) def deriv(self, model): - """Jacobian of the regularization function evaluated for the model provided. + r"""Jacobian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + For a model :math:`\mathbf{m}` consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Jacobian has the form: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} Parameters ---------- - model : list of (n_param, ) numpy.ndarray - The models for which the gradient is evaluated. + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. Returns ------- @@ -273,14 +295,38 @@ def deriv(self, model): ] def deriv2(self, model, v=None): - """Hessian of the regularization function evaluated for the model provided. + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: + For a model :math:`\mathbf{m}` consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} Parameters ---------- - model : list of (n_param, ) numpy.ndarray - The models for which the Hessian is evaluated. + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. v : None, (n_param, ) numpy.ndarray (optional) - A vector + A numpy array to model the Hessian by. Returns ------- diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index b546e997fd..b1926b0759 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -177,7 +177,7 @@ def __call__(self, model): Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the function is evaluated. + The model; a vector array containing all physical properties. Returns ------- @@ -196,12 +196,27 @@ def __call__(self, model): return np.sum(sq) def deriv(self, model): - """Jacobian of the regularization function evaluated for the model provided. + r"""Jacobian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + For a model :math:`\mathbf{m}` consisting of multiple physical properties + :math:`\mathbf{m_1}, \; \mathbf{m_2}, \; ...` such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} + + The Jacobian has the form: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \\ \vdots \end{bmatrix} Parameters ---------- - model : list of (n_param, ) numpy.ndarray - The models for which the gradient is evaluated. + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. Returns ------- @@ -226,12 +241,39 @@ def deriv(self, model): return np.concatenate(ps) def deriv2(self, model, v=None): - """Hessian of the regularization function evaluated for the model provided. + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the model parameters. + For a model :math:`\mathbf{m}` consisting of multiple physical properties + :math:`\mathbf{m_1}, \; \mathbf{m_2}, \; ...` such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} & + \cdots \\ + \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} & \; \\ + \vdots & \; & \ddots + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} Parameters ---------- - model : numpy.ndarray - Stacked array of individual models + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. v : numpy.ndarray, optional An array to multiply the Hessian by. diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index c2724aef3f..ae02a73eca 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -234,7 +234,7 @@ def Pafz(self) -> sp.csr_matrix: def average_face_to_cell(self) -> sp.csr_matrix: """Averaging operator from faces to cell centers. - Built from :py:attr:`discretize.operators.differential_operators.DiffOperators.average_face_to_cell`. + Built from :py:property:`~discretize.operators.differential_operators.DiffOperators.average_face_to_cell`. Returns ------- @@ -253,7 +253,7 @@ def aveFx2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active x-faces. Modified from the transpose of - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFx2CC`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFx2CC`; an operator that projects from all x-faces to all cell centers. Returns @@ -276,7 +276,7 @@ def aveCC2Fx(self) -> sp.csr_matrix: """Averaging operator from active x-faces to active cell centers. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fx`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fx`; an operator that projects from all x-faces to all cell centers. Returns @@ -300,7 +300,7 @@ def aveFy2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active y-faces. Modified from the transpose of - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFy2CC`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFy2CC`; an operator that projects from y-faces to cell centers. Returns @@ -325,7 +325,7 @@ def aveCC2Fy(self) -> sp.csr_matrix: """Averaging operator from active y-faces to active cell centers. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fy`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fy`; an operator that projects from all y-faces to all cell centers. Returns @@ -351,7 +351,7 @@ def aveFz2CC(self) -> sp.csr_matrix: """Averaging operator from active cell centers to active z-faces. Modified from the transpose of - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveFz2CC`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFz2CC`; an operator that projects from z-faces to cell centers. Returns @@ -374,7 +374,7 @@ def aveCC2Fz(self) -> sp.csr_matrix: """Averaging operator from active z-faces to active cell centers. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.aveCC2Fz`; + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fz`; an operator that projects from all z-faces to all cell centers. Returns @@ -410,7 +410,7 @@ def base_length(self) -> float: def cell_gradient(self) -> sp.csr_matrix: """Cell gradient operator (cell centers to faces). - Built from :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient`. + Built from :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient`. Returns ------- @@ -432,7 +432,7 @@ def cell_gradient_x(self) -> sp.csr_matrix: Cell centered x-derivative operator that maps from active cells to active x-faces. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_x`. + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_x`. Returns ------- @@ -462,7 +462,7 @@ def cell_gradient_y(self) -> sp.csr_matrix: Cell centered y-derivative operator that maps from active cells to active y-faces. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_y`. + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_y`. Returns ------- @@ -492,7 +492,7 @@ def cell_gradient_z(self) -> sp.csr_matrix: Cell centered z-derivative operator that maps from active cells to active z-faces. Modified from - :py:attr:`discretize.operators.differential_operators.DiffOperators.cell_gradient_z`. + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_z`. Returns ------- diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 531633e0ca..4192a325f8 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -8,10 +8,11 @@ class BaseVectorRegularization(BaseRegularization): - """Base regularizer for models where each value is a vector. + """Base regularization class for models defined by vector quantities. - Used when your model has a multiple parameters for each cell. This can be helpful if - your model is made up of vector values in each cell or it is an anisotropic model. + The ``BaseVectorRegularization`` class defines properties and methods used + by regularization classes for inversion to recover vector quantities. + It is not directly used to constrain inversions. """ @property @@ -28,7 +29,7 @@ def _weights_shapes(self) -> list[tuple[int]]: class CrossReferenceRegularization(Smallness, BaseVectorRegularization): - r"""Vector regularization with a reference direction. + r"""Cross reference regularization for inversion to recover vector quantities. This regularizer measures the magnitude of the cross product of the vector model with a reference vector model. This encourages the vectors in the model to point @@ -54,15 +55,110 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): (n_cells, dim), meaning it is dependent on the vector component, it will compute the geometric mean of the component weights per cell and use that as a weight. **kwargs - Arguments passed on to the parent classes: :py:class`.Smallness` and - :py:class`.BaseVectorRegularization`. + Arguments passed on to the parent classes: :py:class:`.Smallness` and + :py:class:`.BaseVectorRegularization`. Notes ----- - The continuous form of this regularization looks like: + Consider the case where the model is a vector quantity :math:`\vec{m}(r)`. + The regularization function (objective function) for cross-reference + regularization is given by: .. math:: - \phi_{cross}(m) = \int_{V} ||\vec{m} \times \vec{m}_{ref}||^2 dV + \phi (\vec{m}) = \frac{1}{2} \int_\Omega \, \vec{w}(r) \, \cdot \, + \Big [ \vec{m}(r) \, \times \, \vec{m}^{(ref)}(r) \Big ]^2 \, dv + + where :math:`\vec{m}^{(ref)}(r)` is the reference model vector and :math:`\vec{w}(r)` + is a user-defined weighting function. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discretized approximation for the regularization + function (objective function) is given by: + + .. math:: + \phi (\vec{m}) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \cdot \, + \Big [ \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big ]^2 + + where :math:`\tilde{m}_i` are the model vectors at cell centers and + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply any user-defined weighting. + + In practice, we frequently define the model :math:`\mathbf{m}` as a discrete + vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} + + where :math:`\mathbf{m_p}`, :math:`\mathbf{m_s}` and :math:`\mathbf{m_t}` represent vector + components in the primary, secondary and tertiary directions at cell centers, respectively. + The cross product between :math:`\mathbf{m}` and a similar reference vector + :math:`\mathbf{m^{ref}}` is a linear operation of the form: + + .. math:: + \mathbf{m} \times \mathbf{m^{ref}} = \mathbf{X m} + \begin{bmatrix} + \mathbf{0} & -\boldsymbol{\Lambda_s} & \boldsymbol{\Lambda_t} \\ + \boldsymbol{\Lambda_p} & \mathbf{0} & -\boldsymbol{\Lambda_t} \\ + -\boldsymbol{\Lambda_p} & \boldsymbol{\Lambda_s} & \mathbf{0} + \end{bmatrix} + \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} + + where + + .. math: + \boldsymbol{\Lambda_j} = \textrm{diag} \Big ( \mathbf{m_j^{(red)}} \Big ) + \;\;\;\; \textrm{for} \; j=p,s,t + + The discrete regularization function in linear form is given by: + + .. math:: + \phi (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W X m} \, \Big \|^2 + + where + + - :math:`\boldsymbol{\Lambda}` applies the cross-products, and + - :math:`\mathbf{W}` is the weighting matrix. + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. Each individual set of cell weights acts independently on the + the directional components of the cross-product and is a discrete vector of the form: + + .. math:: + \mathbf{w_j} = \begin{bmatrix} \mathbf{w_p} \\ \mathbf{w_s} \\ \mathbf{w_t} \end{bmatrix} + + The weighting applied within the objective function is given by: + + .. math:: + \mathbf{\tilde{w}} = \big ( \mathbf{e_3 \otimes v} ) \odot \prod_j \mathbf{w_j} + + where + + - :math:`\mathbf{e_3}` is a vector of ones of length 3, + - :math:`\otimes` is the Kronecker product, and + - :math:`\mathbf{v}` are the cell volumes. + + The weighting matrix used to apply the weights for smallness regularization is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as a ``list`` of (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = Smallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + """ def __init__( @@ -85,11 +181,12 @@ def _nC_residual(self): @property def ref_dir(self): - """The reference direction model + """The reference direction model. Returns ------- (n_active, dim) numpy.ndarray + The reference direction model. """ return self._ref_dir @@ -123,13 +220,117 @@ def ref_dir(self, value): self._X = X def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For cross reference regularization, the regularization kernel function is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{X}` + carries out the cross-product with a reference vector model. + For a more detailed description, see the *Notes* section below. + + Parameters + ---------- + m : numpy.ndarray + The vector model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated for the model provided. + + Notes + ----- + The objective function for cross reference regularization is given by: + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W X m} \, \Big \|^2 + + where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), + :math:`\mathbf{X}` carries out the cross-product with a reference vector model, and :math:`\mathbf{W}` is + the weighting matrix. See the :class:`CrossReferenceRegularization` class documentation for more detail. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + """ return self._X @ (self.mapping * m) def f_m_deriv(self, m): + r"""Derivative of the regularization kernel function. + + For ``CrossReferenceRegularization``, the derivative of the regularization kernel function + with respect to the model is given by: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{X} + + where :math:`\mathbf{X}` is a linear operator that carries out the + cross-product with a reference vector model. + + Parameters + ---------- + m : numpy.ndarray + The vector model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + + Notes + ----- + The objective function for cross reference regularization is given by: + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} + \Big \| \mathbf{W X m} \, \Big \|^2 + + where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), + :math:`\mathbf{X}` carries out the cross-product with a reference vector model, and :math:`\mathbf{W}` is + the weighting matrix. See the :class:`CrossReferenceRegularization` class documentation for more detail. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + Thus, the derivative with respect to the model is: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{X} + + """ return self._X @ self.mapping.deriv(m) @property def W(self): + r"""Weighting matrix. + + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`CrossReferenceRegularization` class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. + """ if getattr(self, "_W", None) is None: mesh = self.regularization_mesh nC = mesh.nC From 0bc1ad319a0d5e935f300935fc0375a03e806dff Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sat, 3 Jun 2023 22:06:34 -0700 Subject: [PATCH 125/455] remove test of discretize function --- tests/base/test_utils.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index c8bc597638..541ed1642b 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -24,36 +24,12 @@ surface2ind_topo, ) import discretize -from discretize.tests import check_derivative TOL = 1e-8 np.random.seed(25) -class TestCheckDerivative(unittest.TestCase): - def test_simplePass(self): - def simplePass(x): - return np.sin(x), sdiag(np.cos(x)) - - passed = check_derivative(simplePass, np.random.randn(5), plotIt=False) - self.assertTrue(passed, True) - - def test_simpleFunction(self): - def simpleFunction(x): - return np.sin(x), lambda xi: sdiag(np.cos(x)) * xi - - passed = check_derivative(simpleFunction, np.random.randn(5), plotIt=False) - self.assertTrue(passed, True) - - def test_simpleFail(self): - def simpleFail(x): - return np.sin(x), -sdiag(np.cos(x)) - - passed = check_derivative(simpleFail, np.random.randn(5), plotIt=False) - self.assertTrue(not passed, True) - - class TestCounter(unittest.TestCase): def test_simpleFail(self): class MyClass(object): From 3d2e6252f6124b60fba06794d8585e8ef6ca0857 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 09:38:22 -0700 Subject: [PATCH 126/455] install latest discretize --- .azure-pipelines/matrix.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index d4e275d169..5c541ed711 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -36,6 +36,14 @@ jobs: pip install pytest-azurepipelines displayName: Create Anaconda testing environment + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + source "${HOME}/conda/etc/profile.d/mamba.sh" + conda activate simpeg-test + mamba install -c conda-forge cython + pip install git+https://github.com/simpeg/discretize + displayName: Install latest discretize + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test From 7759fc1d2de3bde34423e2069cc6f662e76e567f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 09:48:44 -0700 Subject: [PATCH 127/455] add -y --- .azure-pipelines/matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 5c541ed711..eb42475116 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -40,7 +40,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" source "${HOME}/conda/etc/profile.d/mamba.sh" conda activate simpeg-test - mamba install -c conda-forge cython + mamba install -y -c conda-forge cython pip install git+https://github.com/simpeg/discretize displayName: Install latest discretize From b180c954af9c3330e1f11d865fdc51e1711b391d Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 10:00:18 -0700 Subject: [PATCH 128/455] remove sphinx-toolbox --- docs/conf.py | 1 - environment_test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 24a4527520..69358c9ad4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,6 @@ "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", - "sphinx_toolbox.collapse", "sphinx_gallery.gen_gallery", "sphinx.ext.todo", "sphinx.ext.linkcode", diff --git a/environment_test.yml b/environment_test.yml index 3622dd2175..59d59bc19f 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -18,7 +18,6 @@ dependencies: - h5py - sphinx - sphinx-gallery>=0.1.13 - - sphinx-toolbox - sphinxcontrib-apidoc - pydata-sphinx-theme - nbsphinx From bf1e7128d737ba2b798464308b640bc069842a86 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 10:56:13 -0700 Subject: [PATCH 129/455] update mag test --- tests/pf/test_mag_inversion_linear_Octree.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index 8f4c70e4e3..cd6e6fd0ba 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -2,7 +2,7 @@ import unittest import numpy as np -from discretize.utils import meshutils, active_from_xyz +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz from SimPEG import ( directives, maps, @@ -59,14 +59,14 @@ def setUp(self): survey = mag.Survey(srcField) # self.mesh.finalize() - self.mesh = meshutils.mesh_builder_xyz( + self.mesh = mesh_builder_xyz( xyzLoc, h, padding_distance=padDist, mesh_type="TREE", ) - self.mesh = meshutils.refine_tree_xyz( + self.mesh = refine_tree_xyz( self.mesh, topo, method="surface", From deff39e76c23c68636e9a77a28d54d80c377fdf2 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 11:04:11 -0700 Subject: [PATCH 130/455] call getJ --- .../static/induced_polarization/simulation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index 879305a5d9..22e588d08b 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -1,22 +1,21 @@ -from .....electromagnetics.static.induced_polarization.simulation import ( - BaseIPSimulation as Sim, -) - import dask.array as da +from .....electromagnetics.static.induced_polarization.simulation import BaseIPSimulation as Sim + def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ """ if getattr(self, "_gtgdiag", None) is None: + J = self.getJ(m, f=f) # Need to check if multiplying weights makes sense if W is None: W = self._scale else: W = self._scale * W.diagonal() w = da.from_array(W)[:, None] - self._gtgdiag = da.sum((w * self.getJ(m, f=f)) ** 2, axis=0).compute() + self._gtgdiag = da.sum((w * J) ** 2, axis=0).compute() return self._gtgdiag From c2d78bedd04345592db6d89f1b605928012be0cd Mon Sep 17 00:00:00 2001 From: Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> Date: Mon, 5 Jun 2023 11:12:57 -0700 Subject: [PATCH 131/455] Update simulation.py --- .../static/induced_polarization/simulation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index 22e588d08b..1f21ddd1d4 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -1,7 +1,8 @@ -import dask.array as da - -from .....electromagnetics.static.induced_polarization.simulation import BaseIPSimulation as Sim +from .....electromagnetics.static.induced_polarization.simulation import ( + BaseIPSimulation as Sim, +) +import dask.array as da def dask_getJtJdiag(self, m, W=None, f=None): """ From ba3be5b1a707a8cbba17310b211189dbf90f0817 Mon Sep 17 00:00:00 2001 From: Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> Date: Mon, 5 Jun 2023 11:13:16 -0700 Subject: [PATCH 132/455] Update simulation.py --- .../electromagnetics/static/induced_polarization/simulation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index 1f21ddd1d4..f57ef9ebfd 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -4,6 +4,7 @@ import dask.array as da + def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ From 94ae4fc35247b0cbf3ecec2647298e3db466a74a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 11:14:34 -0700 Subject: [PATCH 133/455] slightly adjust test --- tests/flow/test_Richards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 81c3d8ccfc..0c750a4fce 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -108,7 +108,7 @@ def _dotest_sensitivity_full(self): print("Testing Richards Derivative FULL dim={}".format(self.mesh.dim)) J = self.prob.Jfull(self.mtrue) passed = check_derivative( - lambda m: [self.prob.dpred(m), J], self.mtrue, num=4, plotIt=False + lambda m: [self.prob.dpred(m), J], self.mtrue, num=3, plotIt=False ) self.assertTrue(passed, True) From 1c3f8b2219a00be1ac40bc738b83327dcff3a2db Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 11:50:31 -0700 Subject: [PATCH 134/455] make tests smaller --- tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py | 12 ++++++------ tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py | 10 +++++----- tests/em/tdem/test_TDEM_crosscheck.py | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 6e5325b3cf..65d9271196 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -18,11 +18,11 @@ def get_mesh(): - cs = 5.0 - ncx = 8 - ncy = 8 - ncz = 8 - npad = 4 + cs = 10.0 + ncx = 4 + ncy = 4 + ncz = 4 + npad = 2 return discretize.TensorMesh( [ @@ -131,7 +131,7 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) - tests.check_derivative(derChk, self.m, plotIt=False, num=2, eps=1e-20) + tests.check_derivative(derChk, self.m, plotIt=False, num=3, eps=1e-20) def JvecVsJtvecTest(self, rxcomp): np.random.seed(4) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 145a011b80..15cc3bfe19 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -15,11 +15,11 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): np.random.seed(10) - cs = 5.0 - ncx = 8 - ncy = 8 - ncz = 8 - npad = 3 + cs = 10.0 + ncx = 4 + ncy = 4 + ncz = 4 + npad = 2 pf = 1.3 mesh = discretize.TensorMesh( diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index 853d4e5c70..8f43792056 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -17,11 +17,11 @@ def setUp_TDEM( ): # set a seed so that the same conductivity model is used for all runs np.random.seed(25) - cs = 5.0 - ncx = 8 - ncy = 8 - ncz = 8 - npad = 4 + cs = 10.0 + ncx = 4 + ncy = 4 + ncz = 4 + npad = 2 # hx = [(cs, ncx), (cs, npad, 1.3)] # hz = [(cs, npad, -1.3), (cs, ncy), (cs, npad, 1.3)] mesh = discretize.TensorMesh( From 218b72a7ddb6807998588a78b7d0cfe9352d854a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 12:18:59 -0700 Subject: [PATCH 135/455] mark expected errors --- tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 65d9271196..843fd2fa3b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -7,6 +7,7 @@ from SimPEG.electromagnetics import utils from scipy.interpolate import interp1d from pymatsolver import Pardiso as Solver +import pytest plotIt = False @@ -71,12 +72,14 @@ def setUpClass(self): time_steps = [(1e-3, 5), (1e-4, 5), (5e-5, 10), (5e-5, 10), (1e-4, 10)] t_mesh = discretize.TensorMesh([time_steps]) times = t_mesh.nodes_x + np.random.rand(412) self.survey = get_survey(times, self.t0) self.prob = get_prob( mesh, mapping, self.formulation, survey=self.survey, time_steps=time_steps ) self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) + self.m *= 0.25 * np.random.rand(*self.m.shape) + 1 print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -101,7 +104,7 @@ def get_rx(self, rxcomp): timerx = self.t0 + np.logspace(-5, -3, 20) return getattr(tdem.Rx, "Point{}".format(rxcomp[:-1]))( - np.array([[rxOffset, 0.0, 0.0]]), timerx, rxcomp[-1] + np.array([[rxOffset, 0.0, 0.0]]), timerx, orientation=rxcomp[-1] ) def set_receiver_list(self, rxcomp): @@ -186,9 +189,11 @@ class DerivAdjoint_B(Base_DerivAdjoint_Test): if testDeriv: + @pytest.mark.xfail def test_Jvec_b_bx(self): self.JvecTest("MagneticFluxDensityx") + @pytest.mark.xfail def test_Jvec_b_bz(self): self.JvecTest("MagneticFluxDensityz") @@ -224,15 +229,19 @@ class DerivAdjoint_H(Base_DerivAdjoint_Test): if testDeriv: + @pytest.mark.xfail def test_Jvec_h_hx(self): self.JvecTest("MagneticFieldx") + @pytest.mark.xfail def test_Jvec_h_hz(self): self.JvecTest("MagneticFieldz") + @pytest.mark.xfail def test_Jvec_h_dhdtx(self): self.JvecTest("MagneticFieldTimeDerivativex") + @pytest.mark.xfail def test_Jvec_h_dhdtz(self): self.JvecTest("MagneticFieldTimeDerivativez") From 11513eebbc21e7e7332ede683c07211a4dfa2c02 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 12:23:39 -0700 Subject: [PATCH 136/455] add test --- tests/dask/test_IP_jvecjtvecadj_dask.py | 127 +++++++++++++++++++++--- 1 file changed, 112 insertions(+), 15 deletions(-) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index c989089db1..1a905b3782 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -1,23 +1,120 @@ +import os +import shutil +import tarfile import unittest -import discretize + +import discretize as ds import numpy as np import SimPEG.dask # noqa: F401 -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc +from SimPEG import data_misfit, inverse_problem, inversion, maps, optimization, regularization, tests, utils from SimPEG.electromagnetics import induced_polarization as ip -import shutil +from SimPEG.electromagnetics import resistivity as dc +from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc np.random.seed(30) +class IPProblemTests2DN(unittest.TestCase): + def setUp(self): + # storage bucket where we have the data + data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcip2d.tar.gz" + + # download the data + downloaded_data = utils.download(data_source, overwrite=True) + + # unzip the tarfile + tar = tarfile.open(downloaded_data, "r") + tar.extractall() + tar.close() + + # path to the directory containing our data + dir_path = downloaded_data.split(".")[0] + os.path.sep + + # files to work with + topo_filename = dir_path + "topo_xyz.txt" + dc_data_filename = dir_path + "dc_data.obs" + ip_data_filename = dir_path + "ip_data.obs" + + # Load topo + topo_xyz = np.loadtxt(str(topo_filename)) + # define the 2D topography along the survey line. + topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) + # Load datas + dc_data = read_dcip2d_ubc(dc_data_filename, "volt", "general") + ip_data = read_dcip2d_ubc(ip_data_filename, "apparent_chargeability", "general") + # assign uncertainties + dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) + ip_data.standard_deviation = 5e-3 * np.ones_like(ip_data.dobs) + # mesh + cs = 10.0 + mesh = ds.TensorMesh( + [ + [(cs, 20, -1.3), (cs, 20, 1.3)], + [(cs, 20, -1.3), (cs, 20, 1.3)], + ], + "CN", + ) + # Find cells that lie below surface topography + ind_active = ds.utils.active_from_xyz(mesh, topo_2d) + # Shift electrodes to the surface of discretized topography + dc_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") + ip_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") + + # Define conductivity model in S/m (or resistivity model in Ohm m) + air_conductivity = 1e-8 + background_conductivity = 1e-2 + active_map = maps.InjectActiveCells(mesh, ind_active, air_conductivity) + nC = int(ind_active.sum()) + # Define model + conductivity_model = background_conductivity * np.ones(nC) + + simulation = ip.simulation.Simulation2DNodal( + mesh=mesh, survey=ip_data.survey, sigma=conductivity_model, etaMap=active_map + ) + mSynth = np.ones(mesh.nC) * 0.1 + # test without calling make_synthetic_data first to simulate real data case + dobs = read_dcip2d_ubc(ip_data_filename, "apparent_chargeability", "general") + # Now set up the problem to do some minimization + dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) + reg = regularization.WeightedLeastSquares(mesh) + opt = optimization.InexactGaussNewton(maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5) + invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) + inv = inversion.BaseInversion(invProb) + + self.inv = inv + self.reg = reg + self.p = simulation + self.mesh = mesh + self.m0 = mSynth + self.survey = ip_data.survey + self.dmis = dmis + self.conductivity_model = conductivity_model + + def test_misfit(self): + passed = tests.check_derivative( + lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], + self.m0, + plotIt=False, + num=3, + ) + self.assertTrue(passed) + + def test_adjoint(self): + # Adjoint Test + v = np.random.rand(self.mesh.nC) + w = np.random.rand(self.survey.nD) + wtJv = w.dot(self.p.Jvec(self.m0, v)) + vtJtw = v.dot(self.p.Jtvec(self.m0, w)) + passed = np.abs(wtJv - vtJtw) < 1e-10 + print("Adjoint Test", np.abs(wtJv - vtJtw), passed) + self.assertTrue(passed) + + def test_dataObj(self): + passed = tests.check_derivative(lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3) + self.assertTrue(passed) + + class IPProblemTestsCC(unittest.TestCase): def setUp(self): aSpacing = 2.5 @@ -26,7 +123,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -96,7 +193,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -165,7 +262,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -245,7 +342,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], From 7a401b949dcf49abc2c45b512a77bf0110c53ee8 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 12:34:47 -0700 Subject: [PATCH 137/455] black --- tests/dask/test_IP_jvecjtvecadj_dask.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 1a905b3782..5eefa41e6c 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -7,7 +7,16 @@ import numpy as np import SimPEG.dask # noqa: F401 -from SimPEG import data_misfit, inverse_problem, inversion, maps, optimization, regularization, tests, utils +from SimPEG import ( + data_misfit, + inverse_problem, + inversion, + maps, + optimization, + regularization, + tests, + utils, +) from SimPEG.electromagnetics import induced_polarization as ip from SimPEG.electromagnetics import resistivity as dc from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc @@ -70,7 +79,10 @@ def setUp(self): conductivity_model = background_conductivity * np.ones(nC) simulation = ip.simulation.Simulation2DNodal( - mesh=mesh, survey=ip_data.survey, sigma=conductivity_model, etaMap=active_map + mesh=mesh, + survey=ip_data.survey, + sigma=conductivity_model, + etaMap=active_map, ) mSynth = np.ones(mesh.nC) * 0.1 # test without calling make_synthetic_data first to simulate real data case @@ -78,7 +90,9 @@ def setUp(self): # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) - opt = optimization.InexactGaussNewton(maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5) + opt = optimization.InexactGaussNewton( + maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5 + ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -111,7 +125,9 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - passed = tests.check_derivative(lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3) + passed = tests.check_derivative( + lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + ) self.assertTrue(passed) From 76025add6907c3b9f64f4bfa635fd3f71c468aea Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 13:21:53 -0700 Subject: [PATCH 138/455] make scale a property --- .../static/induced_polarization/simulation.py | 8 +-- .../static/induced_polarization/simulation.py | 52 ++++++++++--------- tests/dask/test_IP_jvecjtvecadj_dask.py | 5 +- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index f57ef9ebfd..9d9bec633f 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -1,9 +1,9 @@ +import dask.array as da + from .....electromagnetics.static.induced_polarization.simulation import ( BaseIPSimulation as Sim, ) -import dask.array as da - def dask_getJtJdiag(self, m, W=None, f=None): """ @@ -13,9 +13,9 @@ def dask_getJtJdiag(self, m, W=None, f=None): J = self.getJ(m, f=f) # Need to check if multiplying weights makes sense if W is None: - W = self._scale + W = self.scale else: - W = self._scale * W.diagonal() + W = self.scale * W.diagonal() w = da.from_array(W)[:, None] self._gtgdiag = da.sum((w * J) ** 2, axis=0).compute() diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/electromagnetics/static/induced_polarization/simulation.py index 66412fb2f7..a0ed887ec5 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/electromagnetics/static/induced_polarization/simulation.py @@ -1,14 +1,13 @@ import numpy as np import scipy.sparse as sp -from .... import props, maps -from ....data import Data +from .... import maps, props from ....base import BasePDESimulation - -from ..resistivity import Simulation3DCellCentered as DC_3D_CC -from ..resistivity import Simulation3DNodal as DC_3D_N +from ....data import Data from ..resistivity import Simulation2DCellCentered as DC_2D_CC from ..resistivity import Simulation2DNodal as DC_2D_N +from ..resistivity import Simulation3DCellCentered as DC_3D_CC +from ..resistivity import Simulation3DNodal as DC_3D_N class BaseIPSimulation(BasePDESimulation): @@ -40,6 +39,25 @@ def sigmaDeriv(self): def rhoDeriv(self): return sp.diags(self.rho) @ self.etaDeriv + @property + def scale(self): + if getattr(self, "_scale", None) is None: + scale = Data(self.survey, np.ones(self.survey.nD)) + if self._f is None: + # re-uses the DC simulation's fields method + self._f = super().fields(None) + try: + f = self.fields_to_space(self._f) + except AttributeError: + f = 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": + scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) + self._scale = scale.dobs + return self._scale + eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") def __init__( @@ -70,22 +88,6 @@ def __init__( 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.ones(self.survey.nD)) - try: - f = self.fields_to_space(self._f) - except AttributeError: - f = 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": - scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) - self._scale = scale.dobs self._pred = self.forward(m, f=self._f) @@ -110,22 +112,22 @@ def getJtJdiag(self, m, W=None, f=None): if getattr(self, "_gtgdiag", None) is None: J = self.getJ(m, f=f) if W is None: - W = self._scale**2 + W = self.scale**2 else: - W = (self._scale * W.diagonal()) ** 2 + 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) + 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) + return super().Jtvec(m, v * self.scale, f) @property def deleteTheseOnModelUpdate(self): diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 5eefa41e6c..8d68b2eb43 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -84,7 +84,7 @@ def setUp(self): sigma=conductivity_model, etaMap=active_map, ) - mSynth = np.ones(mesh.nC) * 0.1 + mSynth = np.ones(nC) * 0.1 # test without calling make_synthetic_data first to simulate real data case dobs = read_dcip2d_ubc(ip_data_filename, "apparent_chargeability", "general") # Now set up the problem to do some minimization @@ -116,8 +116,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.mesh.nC) + v = np.random.rand(len(self.m0)) w = np.random.rand(self.survey.nD) + # J = self.p.getJ(self.m0) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 From 710810dc705b4bc6bb42f3ea401f6d737d121788 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 13:38:20 -0700 Subject: [PATCH 139/455] fix test For some reason, this test is very unstable with respect to the discretization. --- tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 15cc3bfe19..68e3cf4d34 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -19,7 +19,7 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): ncx = 4 ncy = 4 ncz = 4 - npad = 2 + npad = 3 pf = 1.3 mesh = discretize.TensorMesh( @@ -44,8 +44,8 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): rx = getattr(tdem.Rx, "Point{}".format(rxcomp[:-1]))( locations=rxlocs, times=rxtimes, orientation=rxcomp[-1] ) - Aloc = np.r_[-10, 0.0, src_z] - Bloc = np.r_[10, 0.0, src_z] + Aloc = np.r_[-20, 0.0, src_z] + Bloc = np.r_[20, 0.0, src_z] srcloc = np.vstack((Aloc, Bloc)) src = tdem.Src.LineCurrent( From 8be06e190070b707de23bd9dc0e360d11a0c712b Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 14:18:13 -0700 Subject: [PATCH 140/455] `scale` -> `_scale` --- .../static/induced_polarization/simulation.py | 7 ++- .../static/induced_polarization/simulation.py | 44 +++++++++---------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index 9d9bec633f..c3cb74fa0e 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -13,11 +13,10 @@ def dask_getJtJdiag(self, m, W=None, f=None): J = self.getJ(m, f=f) # Need to check if multiplying weights makes sense if W is None: - W = self.scale + W = self._scale else: - W = self.scale * W.diagonal() - w = da.from_array(W)[:, None] - self._gtgdiag = da.sum((w * J) ** 2, axis=0).compute() + W = self._scale * W.diagonal() + self._gtgdiag = da.einsum("i,ij,ij->j", W**2.0, J, axis=0).compute() return self._gtgdiag diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/electromagnetics/static/induced_polarization/simulation.py index a0ed887ec5..88ca15c4a5 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/electromagnetics/static/induced_polarization/simulation.py @@ -1,3 +1,5 @@ +from functools import cached_property + import numpy as np import scipy.sparse as sp @@ -39,24 +41,22 @@ def sigmaDeriv(self): def rhoDeriv(self): return sp.diags(self.rho) @ self.etaDeriv - @property - def scale(self): - if getattr(self, "_scale", None) is None: - scale = Data(self.survey, np.ones(self.survey.nD)) - if self._f is None: - # re-uses the DC simulation's fields method - self._f = super().fields(None) - try: - f = self.fields_to_space(self._f) - except AttributeError: - f = 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": - scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) - self._scale = scale.dobs - return self._scale + @cached_property + def _scale(self): + scale = Data(self.survey, np.ones(self.survey.nD)) + if self._f is None: + # re-uses the DC simulation's fields method + self._f = super().fields(None) + try: + f = self.fields_to_space(self._f) + except AttributeError: + f = self._f + # loop through receivers 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": + scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) + return scale.dobs eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") @@ -112,22 +112,22 @@ def getJtJdiag(self, m, W=None, f=None): if getattr(self, "_gtgdiag", None) is None: J = self.getJ(m, f=f) if W is None: - W = self.scale**2 + W = self._scale**2 else: - W = (self.scale * W.diagonal()) ** 2 + 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) + 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) + return super().Jtvec(m, v * self._scale, f) @property def deleteTheseOnModelUpdate(self): From 2db93454e4fb9dc9654c27d508ec7022d7b98f6b Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 14:21:35 -0700 Subject: [PATCH 141/455] fix typo --- .../electromagnetics/static/induced_polarization/simulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/electromagnetics/static/induced_polarization/simulation.py index 88ca15c4a5..9406d50e61 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/electromagnetics/static/induced_polarization/simulation.py @@ -83,7 +83,6 @@ def __init__( _Jmatrix = None _pred = None - _scale = None def fields(self, m): if self.verbose: From 1a2c7464a60b013b0ffccd1cf71a007d6c4d2da5 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 14:34:15 -0700 Subject: [PATCH 142/455] fix typo --- .../electromagnetics/static/induced_polarization/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py index c3cb74fa0e..466a926482 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py @@ -16,7 +16,7 @@ def dask_getJtJdiag(self, m, W=None, f=None): W = self._scale else: W = self._scale * W.diagonal() - self._gtgdiag = da.einsum("i,ij,ij->j", W**2.0, J, axis=0).compute() + self._gtgdiag = da.einsum("i,ij,ij->j", W**2, J, J).compute() return self._gtgdiag From a7656eac029c24a67e0e0ce153c2165b1e74719a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 15:01:43 -0700 Subject: [PATCH 143/455] undo these changes --- tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 68e3cf4d34..145a011b80 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -15,10 +15,10 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): np.random.seed(10) - cs = 10.0 - ncx = 4 - ncy = 4 - ncz = 4 + cs = 5.0 + ncx = 8 + ncy = 8 + ncz = 8 npad = 3 pf = 1.3 @@ -44,8 +44,8 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): rx = getattr(tdem.Rx, "Point{}".format(rxcomp[:-1]))( locations=rxlocs, times=rxtimes, orientation=rxcomp[-1] ) - Aloc = np.r_[-20, 0.0, src_z] - Bloc = np.r_[20, 0.0, src_z] + Aloc = np.r_[-10, 0.0, src_z] + Bloc = np.r_[10, 0.0, src_z] srcloc = np.vstack((Aloc, Bloc)) src = tdem.Src.LineCurrent( From 645a20e88286c59a4c51c2ef0bdbdf480339ca50 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 15:03:11 -0700 Subject: [PATCH 144/455] update timeout for slower sphinx builds --- .azure-pipelines/matrix.yml | 1 + azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index eb42475116..28a4df24c5 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -21,6 +21,7 @@ jobs: displayName: ${{ os }}_${{ py_vers }}_${{ test }} pool: vmImage: ${{ os }} + timeoutInMinutes: 120 steps: - script: | wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d50c72b28e..3e49e9954b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -73,7 +73,7 @@ stages: vmImage: ubuntu-latest variables: python.version: '3.8' - timeoutInMinutes: 180 + timeoutInMinutes: 240 steps: - script: | git config --global user.name ${GH_NAME} From b7ba863100862b9cbf3671bec199bd87e9b2d49a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 5 Jun 2023 15:08:30 -0700 Subject: [PATCH 145/455] remove latest discretize step --- .azure-pipelines/matrix.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 28a4df24c5..8f5afb8cb6 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -3,7 +3,7 @@ parameters: py_vers: ['3.8'] test: ['tests/em', 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs', + 'tests/docs -s -v', 'tests/examples/test_examples_1.py', 'tests/examples/test_examples_2.py', 'tests/examples/test_examples_3.py', @@ -37,14 +37,6 @@ jobs: pip install pytest-azurepipelines displayName: Create Anaconda testing environment - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - conda activate simpeg-test - mamba install -y -c conda-forge cython - pip install git+https://github.com/simpeg/discretize - displayName: Install latest discretize - - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test From 12ddc7659d86543611da3a4ebff06ceecc8bd9e2 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 15:48:39 -0700 Subject: [PATCH 146/455] add docstring to test --- tests/dask/test_IP_jvecjtvecadj_dask.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 8d68b2eb43..e9dbece1a3 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -25,6 +25,12 @@ class IPProblemTests2DN(unittest.TestCase): + """ + This test builds upon the 2D files used in the IP2D tutorial, with a much smaller mesh. + + It tests IP 2D with dask without calling `make_synthetic_data` first to simulate a real data case. + """ + def setUp(self): # storage bucket where we have the data data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcip2d.tar.gz" From d33745eb31d19436756fbb21df62e99848f0b097 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 5 Jun 2023 16:23:02 -0700 Subject: [PATCH 147/455] fix fields --- .../electromagnetics/static/induced_polarization/simulation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/SimPEG/electromagnetics/static/induced_polarization/simulation.py index 9406d50e61..027d6933bf 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/simulation.py +++ b/SimPEG/electromagnetics/static/induced_polarization/simulation.py @@ -87,6 +87,9 @@ def __init__( 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) self._pred = self.forward(m, f=self._f) From 312d66db7afeeb97b9f2be47b45de54cd92ec6be Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 6 Jun 2023 11:33:19 -0700 Subject: [PATCH 148/455] cleanup of doc errors --- SimPEG/electromagnetics/__init__.py | 6 +++--- .../static/spontaneous_potential/__init__.py | 2 +- SimPEG/fields.py | 13 +++++++------ SimPEG/potential_fields/gravity/__init__.py | 2 +- SimPEG/regularization/__init__.py | 1 + tutorials/07-fdem/plot_fwd_2_fem_cyl.py | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/SimPEG/electromagnetics/__init__.py b/SimPEG/electromagnetics/__init__.py index 1ddc269178..c83bedd42b 100644 --- a/SimPEG/electromagnetics/__init__.py +++ b/SimPEG/electromagnetics/__init__.py @@ -22,9 +22,9 @@ .. autosummary:: :toctree: generated/ - analytics.h[2]AnalyticDipoleT - analytics.h[2]AnalyticCentLoopT - analytics.h[2]AnalyticDipoleF + analytics.hzAnalyticDipoleT + analytics.hzAnalyticCentLoopT + analytics.hzAnalyticDipoleF analytics.getCasingEphiMagDipole analytics.getCasingHrMagDipole analytics.getCasingHzMagDipole diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py b/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py index 686c1c25f6..2fcc59d552 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py +++ b/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py @@ -21,7 +21,7 @@ .. autosummary:: :toctree: generated/ - sources.StreamingPotential + sources.StreamingCurrents Surveys ======= diff --git a/SimPEG/fields.py b/SimPEG/fields.py index 818bc67a12..85178c361f 100644 --- a/SimPEG/fields.py +++ b/SimPEG/fields.py @@ -6,12 +6,13 @@ class Fields: """Fancy Field Storage - .. code::python - fields = Fields( - simulation=simulation, knownFields={"phi": "CC"} - ) - fields[:,'phi'] = phi - print(fields[src0,'phi']) + + Examples + -------- + >>> fields = Fields( + ... simulation=simulation, knownFields={"phi": "CC"} + ... ) + >>> fields[:,'phi'] = phi """ _dtype = float diff --git a/SimPEG/potential_fields/gravity/__init__.py b/SimPEG/potential_fields/gravity/__init__.py index ff77ec99b3..4a9763bc9d 100644 --- a/SimPEG/potential_fields/gravity/__init__.py +++ b/SimPEG/potential_fields/gravity/__init__.py @@ -16,7 +16,7 @@ Simulation3DDifferential Survey, Sources and Receivers -============================ +============================= .. autosummary:: :toctree: generated/ diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 998e1ca73c..5aa2828ab5 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -164,6 +164,7 @@ .. autosummary:: :toctree: generated/ + CrossReferenceRegularization Joint Regularizations diff --git a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py index 8ff4307448..2ccb719b5f 100644 --- a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py +++ b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py @@ -50,7 +50,7 @@ # # Here we define a x-offset borehole survey that consists of a single vertical line # of source-receiver pairs which measred the secondary magnetic flux density -# over a range of frequencies. +# over a range of frequencies. # # Frequencies being predicted (10 Hz to 10000 Hz) From 92accbcf2f2d918b386cd7b30dc492b6bf90fb91 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 6 Jun 2023 11:33:41 -0700 Subject: [PATCH 149/455] turn deprecations into future warnings --- SimPEG/utils/code_utils.py | 28 +++++++++++++++++-------- SimPEG/utils/io_utils/io_utils_pf.py | 20 ++++++++++++++---- SimPEG/utils/model_builder.py | 31 +++++++++++++++++++--------- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/SimPEG/utils/code_utils.py b/SimPEG/utils/code_utils.py index 0241990316..3057431bb3 100644 --- a/SimPEG/utils/code_utils.py +++ b/SimPEG/utils/code_utils.py @@ -1232,21 +1232,31 @@ def validate_active_indices(property_name, index_arr, n_cells): # DEPRECATIONS ############################################################### memProfileWrapper = deprecate_function( - mem_profile_class, "memProfileWrapper", removal_version="0.18.0" + mem_profile_class, "memProfileWrapper", removal_version="0.18.0", future_warn=True +) +setKwargs = deprecate_function( + set_kwargs, "setKwargs", removal_version="0.18.0", future_warn=True +) +printTitles = deprecate_function( + print_titles, "printTitles", removal_version="0.18.0", future_warn=True +) +printLine = deprecate_function( + print_line, "printLine", removal_version="0.18.0", future_warn=True ) -setKwargs = deprecate_function(set_kwargs, "setKwargs", removal_version="0.18.0") -printTitles = deprecate_function(print_titles, "printTitles", removal_version="0.18.0") -printLine = deprecate_function(print_line, "printLine", removal_version="0.18.0") printStoppers = deprecate_function( - print_stoppers, "printStoppers", removal_version="0.18.0" + print_stoppers, "printStoppers", removal_version="0.18.0", future_warn=True ) checkStoppers = deprecate_function( - check_stoppers, "checkStoppers", removal_version="0.18.0" + check_stoppers, "checkStoppers", removal_version="0.18.0", future_warn=True +) +printDone = deprecate_function( + print_done, "printDone", removal_version="0.18.0", future_warn=True +) +callHooks = deprecate_function( + call_hooks, "callHooks", removal_version="0.18.0", future_warn=True ) -printDone = deprecate_function(print_done, "printDone", removal_version="0.18.0") -callHooks = deprecate_function(call_hooks, "callHooks", removal_version="0.18.0") dependentProperty = deprecate_function( - dependent_property, "dependentProperty", removal_version="0.18.0" + dependent_property, "dependentProperty", removal_version="0.18.0", future_warn=True ) asArray_N_x_Dim = deprecate_function( as_array_n_by_dim, "asArray_N_x_Dim", removal_version="0.19.0", future_warn=True diff --git a/SimPEG/utils/io_utils/io_utils_pf.py b/SimPEG/utils/io_utils/io_utils_pf.py index 5e9533f5e3..ec31d8ba5c 100644 --- a/SimPEG/utils/io_utils/io_utils_pf.py +++ b/SimPEG/utils/io_utils/io_utils_pf.py @@ -387,14 +387,26 @@ def write_gg3d_ubc(filename, data_object): readUBCmagneticsObservations = deprecate_method( - read_mag3d_ubc, "readUBCmagneticsObservations", removal_version="0.14.4" + read_mag3d_ubc, + "readUBCmagneticsObservations", + removal_version="0.14.4", + future_warn=True, ) writeUBCmagneticsObservations = deprecate_method( - write_mag3d_ubc, "writeUBCmagneticsObservations", removal_version="0.14.4" + write_mag3d_ubc, + "writeUBCmagneticsObservations", + removal_version="0.14.4", + future_warn=True, ) readUBCgravityObservations = deprecate_method( - read_grav3d_ubc, "readUBCgravityObservations", removal_version="0.14.4" + read_grav3d_ubc, + "readUBCgravityObservations", + removal_version="0.14.4", + future_warn=True, ) writeUBCgravityObservations = deprecate_method( - write_grav3d_ubc, "writeUBCgravityObservations", removal_version="0.14.4" + write_grav3d_ubc, + "writeUBCgravityObservations", + removal_version="0.14.4", + future_warn=True, ) diff --git a/SimPEG/utils/model_builder.py b/SimPEG/utils/model_builder.py index dfcd0fd930..60d2e26afc 100644 --- a/SimPEG/utils/model_builder.py +++ b/SimPEG/utils/model_builder.py @@ -517,40 +517,51 @@ def get_indices_polygon(mesh, pts): ################################################ -addBlock = deprecate_function(add_block, "addBlock", removal_version="0.19.0") +addBlock = deprecate_function( + add_block, "addBlock", removal_version="0.19.0", future_warn=True +) getIndicesBlock = deprecate_function( - get_indices_block, "getIndicesBlock", removal_version="0.19.0" + get_indices_block, "getIndicesBlock", removal_version="0.19.0", future_warn=True ) defineBlock = deprecate_function( - create_block_in_wholespace, "defineBlock", removal_version="0.19.0" + create_block_in_wholespace, + "defineBlock", + removal_version="0.19.0", + future_warn=True, ) defineEllipse = deprecate_function( - create_ellipse_in_wholespace, "defineEllipse", removal_version="0.19.0" + create_ellipse_in_wholespace, + "defineEllipse", + removal_version="0.19.0", + future_warn=True, ) getIndicesSphere = deprecate_function( - get_indices_sphere, "getIndicesSphere", removal_version="0.19.0" + get_indices_sphere, "getIndicesSphere", removal_version="0.19.0", future_warn=True ) defineTwoLayers = deprecate_function( - create_2_layer_model, "defineTwoLayers", removal_version="0.19.0" + create_2_layer_model, "defineTwoLayers", removal_version="0.19.0", future_warn=True ) layeredModel = deprecate_function( - create_layers_model, "layeredModel", removal_version="0.19.0" + create_layers_model, "layeredModel", removal_version="0.19.0", future_warn=True ) randomModel = deprecate_function( - create_random_model, "randomModel", removal_version="0.19.0" + create_random_model, "randomModel", removal_version="0.19.0", future_warn=True ) polygonInd = deprecate_function( - get_indices_polygon, "polygonInd", removal_version="0.19.0" + get_indices_polygon, "polygonInd", removal_version="0.19.0", future_warn=True ) scalarConductivity = deprecate_function( - create_from_function, "scalarConductivity", removal_version="0.19.0" + create_from_function, + "scalarConductivity", + removal_version="0.19.0", + future_warn=True, ) From 5a8264e85af238db9fd7c838f8385a42913f8e0b Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 6 Jun 2023 11:58:35 -0700 Subject: [PATCH 150/455] Remove re-documented discretize functions --- SimPEG/utils/__init__.py | 43 ---------------------------------------- 1 file changed, 43 deletions(-) diff --git a/SimPEG/utils/__init__.py b/SimPEG/utils/__init__.py index 2b6b3c7ba7..49e3e6193a 100644 --- a/SimPEG/utils/__init__.py +++ b/SimPEG/utils/__init__.py @@ -11,15 +11,6 @@ documentation for many details on items. -Coordinates Utility Functions -============================= - -.. autosummary:: - :toctree: generated/ - - rotation_matrix_from_normals - rotate_points_from_normals - Counter Utility Functions ========================= @@ -30,17 +21,6 @@ count timeIt -Curvilinear Utility Functions -============================= - -.. autosummary:: - :toctree: generated/ - - example_curvilinear_grid - face_info - index_cube - volume_tetrahedron - IO Utility Functions ==================== @@ -71,30 +51,12 @@ .. autosummary:: :toctree: generated/ - av - av_extrap cartesian2spherical coterminal - ddx define_plane_from_points - diagEst eigenvalue_by_power_iteration estimate_diagonal - get_subarray - kron3 - ind2sub - inverse_2x2_block_diagonal - inverse_3x3_block_diagonal - inverse_property_tensor - make_property_tensor - mkvc - ndgrid - sdiag - sdinv - speye spherical2cartesian - spzeros - sub2ind unique_rows @@ -104,9 +66,6 @@ .. autosummary:: :toctree: generated/ - closest_points_index - extract_core_mesh - unpack_widths surface2inds @@ -157,7 +116,6 @@ .. autosummary:: :toctree: generated/ - as_array_n_by_dim call_hooks check_stoppers mem_profile_class @@ -169,7 +127,6 @@ deprecate_property hook print_done - printDone print_line print_stoppers print_titles From 98d564574f44309e9652fb9e01b844d0920cd5ee Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 6 Jun 2023 13:07:02 -0700 Subject: [PATCH 151/455] fix indentation --- SimPEG/regularization/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 95f7776ca6..8e3104cf50 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -403,7 +403,7 @@ class Smallness(BaseRegularization): :math:`\mathbf{m_{ref}}` is a reference model, :math:`\mathbf{V}` are square root of cell volumes and :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or - free weights are provided, then it is :code:`diag(np.sqrt(weights))`). + free weights are provided, then it is :code:`diag(np.sqrt(weights))`). **Optional Inputs** From 3f079d946229cfad5b86951c8525fce399fe1c39 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 6 Jun 2023 13:07:10 -0700 Subject: [PATCH 152/455] fix example --- SimPEG/electromagnetics/analytics/FDEM.py | 37 +++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/SimPEG/electromagnetics/analytics/FDEM.py b/SimPEG/electromagnetics/analytics/FDEM.py index d8ce609c32..b34fa85d00 100644 --- a/SimPEG/electromagnetics/analytics/FDEM.py +++ b/SimPEG/electromagnetics/analytics/FDEM.py @@ -9,25 +9,24 @@ def hzAnalyticDipoleF(r, freq, sigma, secondary=True, mu=mu_0): 1988, and the example reproduces their Figure 4.2. - .. plot:: - - import numpy as np - import matplotlib.pyplot as plt - from SimPEG import electromagnetics as EM - freq = np.logspace(-1, 5, 301) - test = EM.analytics.h[2]AnalyticDipoleF( - 100, freq, 0.01, secondary=False) - plt.loglog(freq, test.real, 'C0-', label='Real') - plt.loglog(freq, -test.real, 'C0--') - plt.loglog(freq, test.imag, 'C1-', label='Imaginary') - plt.loglog(freq, -test.imag, 'C1--') - plt.title('Response at $r=100$ m') - plt.xlim([1e-1, 1e5]) - plt.ylim([1e-12, 1e-6]) - plt.xlabel('Frequency (Hz)') - plt.ylabel('$H_z$ (A/m)') - plt.legend(loc=6) - plt.show() + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> from SimPEG import electromagnetics as em + >>> freq = np.logspace(-1, 5, 301) + >>> test = em.analytics.hzAnalyticDipoleF( + >>> 100, freq, 0.01, secondary=False) + >>> plt.loglog(freq, test.real, 'C0-', label='Real') + >>> plt.loglog(freq, -test.real, 'C0--') + >>> plt.loglog(freq, test.imag, 'C1-', label='Imaginary') + >>> plt.loglog(freq, -test.imag, 'C1--') + >>> plt.title('Response at $r=100$ m') + >>> plt.xlim([1e-1, 1e5]) + >>> plt.ylim([1e-12, 1e-6]) + >>> plt.xlabel('Frequency (Hz)') + >>> plt.ylabel('$H_z$ (A/m)') + >>> plt.legend(loc=6) + >>> plt.show() **Reference** From cbb69c5d0355c50194d41921502321a7016c209d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 6 Jun 2023 14:34:31 -0700 Subject: [PATCH 153/455] Add missing line lost in merge --- SimPEG/objective_function.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 2e262fbcb0..d5f8c9f6aa 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -163,6 +163,7 @@ def __add__(self, other): ) objective_functions, multipliers = [], [] for instance in (self, other): + if isinstance(instance, ComboObjectiveFunction) and instance._unpack_on_add: objective_functions += instance.objfcts multipliers += instance.multipliers else: From f71cd3bfd3790c4f5e609680c5f4bdf1f68d7fd7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 6 Jun 2023 14:37:05 -0700 Subject: [PATCH 154/455] Remove additional examples in ComboObjectiveFunction --- SimPEG/objective_function.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index d5f8c9f6aa..be698f218d 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -262,12 +262,6 @@ class ComboObjectiveFunction(BaseObjectiveFunction): >>> combo_2 = combo_1 + objective_fun_c >>> print(len(combo_2)) 2 - - Examples - -------- - >>> objective_fun_a = BaseObjectiveFunction(nP=3) - >>> objective_fun_b = BaseObjectiveFunction(nP=3) - >>> combo = ComboObjectiveFunction([objective_fun_a, objective_fun_b], [1, 0.5]) """ _multiplier_types = (float, None, Zero, np.float64, int, np.integer) From dfa2f04e1349ecb68ae3f2772de422291b1d8579 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 6 Jun 2023 14:37:24 -0700 Subject: [PATCH 155/455] Remove kwargs in ComboObjectiveFunction --- SimPEG/objective_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index be698f218d..781cbdf8a9 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -266,7 +266,7 @@ class ComboObjectiveFunction(BaseObjectiveFunction): _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True, **kwargs): + def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): # Define default lists if None if objfcts is None: objfcts = [] From f44e12b9e5e9482ea98d4c3b0dcae3312cd4fc92 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 6 Jun 2023 14:39:30 -0700 Subject: [PATCH 156/455] Remove unused plotIt argument in test method --- SimPEG/objective_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 781cbdf8a9..f08b61fae3 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -142,7 +142,7 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): **kwargs, ) - def test(self, x=None, num=4, plotIt=False, **kwargs): + def test(self, x=None, num=4, **kwargs): """ Run a convergence test on both the first and second derivatives - they should be second order! From f1a93402ea38db20d34dd6b31e2fd19791448fb8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 6 Jun 2023 15:54:04 -0700 Subject: [PATCH 157/455] Replace assertion error for typeerror in mapping setter --- SimPEG/objective_function.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index f08b61fae3..09447a9623 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -84,9 +84,11 @@ def mapping(self): @mapping.setter def mapping(self, value): - assert isinstance(value, self.map_class), ( - "mapping must be an instance of a {}, not a {}" - ).format(self.map_class, value.__class__.__name__) + if not isinstance(value, self.map_class): + raise TypeError( + f"Invalid mapping of class '{value.__class__.__name__}'. " + f"It must be an instance of {self.map_class.__name__}" + ) self._mapping = value @timeIt From 1d0e3a88ed672c45ae98356f8ad6299574dc4e72 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 7 Jun 2023 11:56:28 -0700 Subject: [PATCH 158/455] Restore the kwargs for now We might want to deal with them later --- SimPEG/objective_function.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 09447a9623..50c047d8a5 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -268,7 +268,7 @@ class ComboObjectiveFunction(BaseObjectiveFunction): _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): + def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True, **kwargs): # Define default lists if None if objfcts is None: objfcts = [] @@ -287,7 +287,7 @@ def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): else: nP = None - super().__init__(nP=nP) + super().__init__(nP=nP, **kwargs) self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add @@ -475,6 +475,7 @@ def __init__( has_fields=False, counter=None, debug=False, + **kwargs, ): # Check if nP and shape of W are consistent if W is not None and nP is not None and nP != W.shape[1]: @@ -483,7 +484,12 @@ def __init__( f"rows ('{W.shape[1]}') of the weights matrix W." ) super().__init__( - nP=nP, mapping=mapping, has_fields=has_fields, debug=debug, counter=counter + nP=nP, + mapping=mapping, + has_fields=has_fields, + debug=debug, + counter=counter, + **kwargs, ) if W is not None and self.nP == "*": self._nP = W.shape[1] From e3852c165e7bfc95df4694c9c9825a5bd0286f34 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 7 Jun 2023 15:55:31 -0700 Subject: [PATCH 159/455] do not sum in for loop (in case an operation fails) --- SimPEG/meta/multiprocessing.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index ac772116fa..704ad1e451 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -247,10 +247,10 @@ def Jtvec(self, m, v, f=None): chunk_v = v[self._data_offsets[i] : self._data_offsets[i + 1]] p.start_jt_vec(chunk_v, field) - jt_vec = 0 + jt_vec = [] for p in self._sim_processes: - jt_vec += p.result() - return jt_vec + jt_vec.append(p.result()) + return np.sum(jt_vec, axis=0) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -264,10 +264,10 @@ def getJtJdiag(self, m, W=None, f=None): for i, (p, field) in enumerate(zip(self._sim_processes, f)): chunk_w = W[self._data_offsets[i] : self._data_offsets[i + 1]] p.start_jtj_diag(chunk_w, field) - jtj_diag = 0.0 + jtj_diag = [] for p in self._sim_processes: - jtj_diag += p.result() - self._jtjdiag = jtj_diag + jtj_diag.append(p.result()) + self._jtjdiag = np.sum(jtj_diag, axis=0) return self._jtjdiag def join(self): @@ -299,10 +299,10 @@ def Jvec(self, m, v, f=None): f = self.fields(m) for p, field in zip(self._sim_processes, f): p.start_j_vec(v, field) - j_vec = 0 + j_vec = [] for p in self._sim_processes: - j_vec += p.result() - return j_vec + j_vec.append(p.result()) + return np.sum(j_vec, axis=0) def Jtvec(self, m, v, f=None): self.model = m @@ -311,10 +311,10 @@ def Jtvec(self, m, v, f=None): for p, field in zip(self._sim_processes, f): p.start_jt_vec(v, field) - jt_vec = 0 + jt_vec = [] for p in self._sim_processes: - jt_vec += p.result() - return jt_vec + jt_vec.append(p.result()) + return np.sum(jt_vec, axis=0) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -323,10 +323,10 @@ def getJtJdiag(self, m, W=None, f=None): f = self.fields(m) for p, field in zip(self._sim_processes, f): p.start_jtj_diag(W, field) - jtj_diag = 0.0 + jtj_diag = [] for p in self._sim_processes: - jtj_diag += p.result() - self._jtjdiag = jtj_diag + jtj_diag.append(p.result()) + self._jtjdiag = np.sum(jtj_diag, axis=0) return self._jtjdiag From ba33ddc47c92780907c80f1514d776e203caad9d Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 7 Jun 2023 16:03:26 -0700 Subject: [PATCH 160/455] set chunk_nd for repeated multi sim --- SimPEG/meta/multiprocessing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 704ad1e451..b0181baa39 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -341,7 +341,6 @@ def __init__(self, simulation, mappings, n_processes=None): n_processes = cpu_count() # split mappings up into chunks - # (Which are currently defined using MetaSimulations) n_sim = len(mappings) chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] for i in range(n_sim % n_processes): @@ -349,6 +348,7 @@ def __init__(self, simulation, mappings, n_processes=None): processes = [] i_start = 0 + chunk_nd = [] for chunk in chunk_sizes: if chunk == 0: continue @@ -356,9 +356,11 @@ def __init__(self, simulation, mappings, n_processes=None): sim_chunk = RepeatedSimulation( self.simulation, self.mappings[i_start:i_end] ) + chunk_nd.append(sim_chunk.survey.nD) p = _SimulationProcess(sim_chunk) processes.append(p) p.start() i_start = i_end + self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) self._sim_processes = processes From 621c9453ab4a23bde5f4815935319c402d803ba2 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 7 Jun 2023 16:02:24 -0700 Subject: [PATCH 161/455] Remove indActive from WeightedLeastSquares kwargs Remove `indActive` from `WeightedLeastSquares` kwargs and assign it to `active_cells`. Explicitly set `self.active_cells` after calling parent's `__init__`. This simplifies the process of setting active_cells: previously the `indActive` argument was being passed to every parent up to `BaseObjectiveFunction`, where the attr was set. This process was a little bit obscure: setting the `active_cells` attribute needs the `objcfts` attribute already defined, so the order in which they are defined is important, but it's decided at parent's levels, and not in `WeightedLeastSquares`. --- SimPEG/regularization/base.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index ab672f66a6..caa304c380 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -1,4 +1,5 @@ from __future__ import annotations +import warnings import numpy as np from discretize.base import BaseMesh @@ -728,8 +729,19 @@ def __init__( f"Value of type {type(mesh)} provided." ) self._regularization_mesh = mesh - if active_cells is not None: - self._regularization_mesh.active_cells = active_cells + + if (key := "indActive") in kwargs: + if active_cells is not None: + raise ValueError( + f"Cannot simultanously pass 'active_cells' and '{key}'. " + "Pass 'active_cells' only." + ) + warnings.warn( + f"The '{key}' argument has been deprecated, please use 'active_cells'. " + "It will be removed in future versions of SimPEG.", + DeprecationWarning, + ) + active_cells = kwargs.pop(key) self.alpha_s = alpha_s if alpha_x is not None: @@ -795,7 +807,10 @@ def __init__( ) else: objfcts = kwargs.pop("objfcts") + super().__init__(objfcts=objfcts, unpack_on_add=False, **kwargs) + if active_cells is not None: + self.active_cells = active_cells self.mapping = mapping self.reference_model = reference_model self.reference_model_in_smooth = reference_model_in_smooth @@ -1070,7 +1085,7 @@ def active_cells(self) -> np.ndarray: def active_cells(self, values: np.ndarray): self.regularization_mesh.active_cells = values active_cells = self.regularization_mesh.active_cells - # notify the objtecive functions that the active_cells changed + # notify the objective functions that the active_cells changed for objfct in self.objfcts: objfct.active_cells = active_cells From e29e2281fec3c27f5e9b13f1b71b1324b7f2a9d6 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 7 Jun 2023 18:13:05 -0700 Subject: [PATCH 162/455] updates to j-formulation using permittivity --- .../frequency_domain/fields.py | 23 ++++++++++++++- .../frequency_domain/simulation.py | 29 ++++++++++--------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/SimPEG/electromagnetics/frequency_domain/fields.py index d39dd3b039..70077b0347 100644 --- a/SimPEG/electromagnetics/frequency_domain/fields.py +++ b/SimPEG/electromagnetics/frequency_domain/fields.py @@ -1087,8 +1087,20 @@ def _hSecondary(self, jSolution, source_list): :return: secondary magnetic field """ - h = self._edgeCurl.T * (self._MfRho * jSolution) + if getattr(self.simulation, "permittivity", None) is not None: + h = np.zeros((self.mesh.n_edges, len(source_list)), dtype=complex) + else: + h = self._edgeCurl.T * (self._MfRho * jSolution) + for i, src in enumerate(source_list): + if getattr(self.simulation, "permittivity", None) is not None: + h[:, i] = self._edgeCurl.T * ( + self.simulation._get_face_admittivity_property_matrix( + src.frequency, invert_model=True + ) + * jSolution[:, i] + ) + h[:, i] *= -1.0 / (1j * omega(src.frequency)) s_m = src.s_m(self.simulation) h[:, i] = h[:, i] + 1.0 / (1j * omega(src.frequency)) * (s_m) @@ -1188,8 +1200,17 @@ def _e(self, jSolution, source_list): :rtype: numpy.ndarray :return: electric field """ + # if getattr(self.simulation, "permittivity", None) is None: return self._MfI * (self._MfRho * self._j(jSolution, source_list)) + # e = np.zeros((self.mesh.n_faces, len(source_list)), dtype=complex) + # for i, source in enumerate(source_list): + # Mfyhati = self.simulation._get_face_admittivity_property_matrix( + # source.frequency, invert_model=True + # ) + # e[:, i] = Mfyhati * mkvc(self._j(jSolution, [source])) + # return self._MfI * e + def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of the electric field with respect to the thing we solved diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index fbe3b18a3f..4c6d1040f3 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -303,8 +303,7 @@ def getA(self, freq): A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma if getattr(self, "permittivity", None) is not None: - MePermittivity = self.MePermittivity - A = A - omega(freq) ** 2 * MePermittivity + A = A - omega(freq) ** 2 * self.MePermittivity return A def getADeriv_sigma(self, freq, u, v, adjoint=False): @@ -635,18 +634,19 @@ def __init__( super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) self.permittivity = permittivity - # def _clear_on_rho_update(self): - # return ( - # super()._clear_on_rho_update + self._Mfrho_permittivity - # ) + def _get_admittivity(self, freq): + return self.sigma + 1j * self.permittivity * omega(freq) - @property - def _Mf_rho_permittivity(self): + def _get_face_admittivity_property_matrix( + self, freq, invert_model=False, invert_matrix=False + ): """ Face inner product matrix with permittivity and resistivity """ - # todo: cache, but clear on update to rho - return self.mesh.get_face_inner_product(self.permittivity * self.rho) + yhat = self._get_admittivity(freq) + return self.mesh.get_face_inner_product( + yhat, invert_model=invert_model, invert_matrix=invert_matrix + ) def getA(self, freq): r""" @@ -667,10 +667,13 @@ def getA(self, freq): C = self.mesh.edge_curl iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - A = C * MeMuI * C.T.tocsr() * MfRho + iomega - if getattr(self, "permittivity", None) is not None: - A = A - omega(freq) ** 2 * self.MfI * self._Mf_rho_permittivity + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + A = C * MeMuI * C.T.tocsr() * Mfyhati + iomega + else: + A = C * MeMuI * C.T.tocsr() * MfRho + iomega if self._makeASymmetric is True: return MfRho.T.tocsr() * A From 0ff0bd44e189119ac3aca83fe992669962cb8c94 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 7 Jun 2023 19:00:23 -0700 Subject: [PATCH 163/455] fill out the simulations using permittivity --- .../frequency_domain/fields.py | 24 +++- .../frequency_domain/simulation.py | 124 ++++++++++++------ 2 files changed, 101 insertions(+), 47 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/SimPEG/electromagnetics/frequency_domain/fields.py index 70077b0347..8c0ca874fa 100644 --- a/SimPEG/electromagnetics/frequency_domain/fields.py +++ b/SimPEG/electromagnetics/frequency_domain/fields.py @@ -763,7 +763,16 @@ def _eSecondary(self, bSolution, source_list): s_e = src.s_e(self.simulation) e[:, i] = e[:, i] + -s_e - return self._MeSigmaI * e + if getattr(self.simulation, "permittivity", None) is not None: + MeyhatI = self.simulation._get_edge_admittivity_property_matrix( + src.frequency, invert_matrix=True + ) + e[:, i] = MeyhatI * e[:, i] + + if getattr(self.simulation, "permittivity", None) is None: + return self._MeSigmaI * e + else: + return e def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ @@ -827,13 +836,16 @@ def _j(self, bSolution, source_list): :return: primary current density """ - j = self._edgeCurl.T * (self._MfMui * bSolution) + if getattr(self.simulation, "permittivity", None) is None: + j = self._edgeCurl.T * (self._MfMui * bSolution) - for i, src in enumerate(source_list): - s_e = src.s_e(self.simulation) - j[:, i] = j[:, i] - s_e + for i, src in enumerate(source_list): + s_e = src.s_e(self.simulation) + j[:, i] = j[:, i] - s_e - return self._MeI * j + return self._MeI * j + else: + return self._MeI * self._MeSigma * self._e(bSolution, source_list) def _jDeriv_u(self, src, du_dm_v, adjoint=False): """ diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 4c6d1040f3..dd6fb27257 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -57,10 +57,19 @@ class BaseFDEMSimulation(BaseEMSimulation): """ fieldsPair = FieldsFDEM + permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") - def __init__(self, mesh, survey=None, forward_only=False, **kwargs): + def __init__( + self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only + if permittivity is not None: + warnings.warn( + "Simulations using permittivity have not yet been thoroughly tested yet, and derivatives are not implemented. Contributions welcome!" + ) + self.permittivity = permittivity @property def survey(self): @@ -94,6 +103,34 @@ def forward_only(self): def forward_only(self, value): self._forward_only = validate_type("forward_only", value, bool) + def _get_admittivity(self, freq): + if getattr(self, "permittivity", None) is not None: + return self.sigma + 1j * self.permittivity * omega(freq) + else: + return self.sigma + + def _get_face_admittivity_property_matrix( + self, freq, invert_model=False, invert_matrix=False + ): + """ + Face inner product matrix with permittivity and resistivity + """ + yhat = self._get_admittivity(freq) + return self.mesh.get_face_inner_product( + yhat, invert_model=invert_model, invert_matrix=invert_matrix + ) + + def _get_edge_admittivity_property_matrix( + self, freq, invert_model=False, invert_matrix=False + ): + """ + Face inner product matrix with permittivity and resistivity + """ + yhat = self._get_admittivity(freq) + return self.mesh.get_edge_inner_product( + yhat, invert_model=invert_model, invert_matrix=invert_matrix + ) + # @profile def fields(self, m=None): """ @@ -242,7 +279,6 @@ def getSourceTerm(self, freq): ############################################################################### -@with_property_mass_matrices("permittivity") class Simulation3DElectricField(BaseFDEMSimulation): r""" By eliminating the magnetic flux density using @@ -272,14 +308,14 @@ class Simulation3DElectricField(BaseFDEMSimulation): _formulation = "EB" fieldsPair = Fields3DElectricField - permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") + # permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + # # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") - def __init__( - self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs - ): - super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) - self.permittivity = permittivity + # def __init__( + # self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + # ): + # super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) + # self.permittivity = permittivity def getA(self, freq): r""" @@ -296,14 +332,15 @@ def getA(self, freq): """ MfMui = self.MfMui - MeSigma = self.MeSigma - C = self.mesh.edge_curl - A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma + if getattr(self, "permittivity", None) is None: + MeSigma = self.MeSigma + A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma + else: + Meyhat = self._get_edge_admittivity_property_matrix(freq) + A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * Meyhat - if getattr(self, "permittivity", None) is not None: - A = A - omega(freq) ** 2 * self.MePermittivity return A def getADeriv_sigma(self, freq, u, v, adjoint=False): @@ -461,11 +498,17 @@ def getA(self, freq): """ MfMui = self.MfMui - MeSigmaI = self.MeSigmaI C = self.mesh.edge_curl iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega + if getattr(self, "permittivity", None) is None: + MeSigmaI = self.MeSigmaI + A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega + else: + MeyhatI = self._get_edge_admittivity_property_matrix( + freq, invert_matrix=True + ) + A = C * (MeyhatI * (C.T.tocsr() * MfMui)) + iomega if self._makeASymmetric: return MfMui.T.tocsr() * A @@ -542,9 +585,15 @@ def getRHS(self, freq): s_m, s_e = self.getSourceTerm(freq) C = self.mesh.edge_curl - MeSigmaI = self.MeSigmaI - RHS = s_m + C * (MeSigmaI * s_e) + if getattr(self, "permittivity", None) is None: + MeSigmaI = self.MeSigmaI + RHS = s_m + C * (MeSigmaI * s_e) + else: + MeyhatI = self._get_edge_admittivity_property_matrix( + freq, invert_matrix=True + ) + RHS = s_m + C * (MeyhatI * s_e) if self._makeASymmetric is True: MfMui = self.MfMui @@ -634,20 +683,6 @@ def __init__( super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) self.permittivity = permittivity - def _get_admittivity(self, freq): - return self.sigma + 1j * self.permittivity * omega(freq) - - def _get_face_admittivity_property_matrix( - self, freq, invert_model=False, invert_matrix=False - ): - """ - Face inner product matrix with permittivity and resistivity - """ - yhat = self._get_admittivity(freq) - return self.mesh.get_face_inner_product( - yhat, invert_model=invert_model, invert_matrix=invert_matrix - ) - def getA(self, freq): r""" System matrix @@ -729,11 +764,6 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return Aderiv def getADeriv(self, freq, u, v, adjoint=False): - if getattr(self, "permittivity", None) is not None: - warnings.warn( - "Derivatives not yet implemented for simulations that include permittivity" - ) - if adjoint and self._makeASymmetric: v = self.MfRho * v @@ -856,10 +886,16 @@ def getA(self, freq): """ MeMu = self.MeMu - MfRho = self.MfRho C = self.mesh.edge_curl - return C.T.tocsr() * (MfRho * C) + 1j * omega(freq) * MeMu + if getattr(self, "permittivity", None) is None: + MfRho = self.MfRho + return C.T.tocsr() * (MfRho * C) + 1j * omega(freq) * MeMu + else: + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu def getADeriv_rho(self, freq, u, v, adjoint=False): r""" @@ -916,9 +952,15 @@ def getRHS(self, freq): s_m, s_e = self.getSourceTerm(freq) C = self.mesh.edge_curl - MfRho = self.MfRho - return s_m + C.T * (MfRho * s_e) + if getattr(self, "permittivity", None) is None: + MfRho = self.MfRho + return s_m + C.T * (MfRho * s_e) + else: + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + return s_m + C.T * (Mfyhati * s_e) def getRHSDeriv(self, freq, src, v, adjoint=False): """ From 2685efeaa59b25b758bc2b5d351698cff39236e2 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 7 Jun 2023 19:11:42 -0700 Subject: [PATCH 164/455] only run meta test (temporarily) and increase vebose --- .azure-pipelines/matrix.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index d4e275d169..24b61baf83 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -1,16 +1,18 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] - test: ['tests/em', - 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs', - 'tests/examples/test_examples_1.py', - 'tests/examples/test_examples_2.py', - 'tests/examples/test_examples_3.py', - 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', - 'tests/examples/test_tutorials_3.py', - 'tests/pf', - 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. + test: [ +# 'tests/em', +# 'tests/base tests/flow tests/seis tests/utils tests/meta', +# 'tests/docs', +# 'tests/examples/test_examples_1.py', +# 'tests/examples/test_examples_2.py', +# 'tests/examples/test_examples_3.py', +# 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', +# 'tests/examples/test_tutorials_3.py', +# 'tests/pf', +# 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. + '-s -v tests/meta' ] jobs: From efd2df3ae403f630b29f8ba7d455877acd912b1e Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 7 Jun 2023 23:37:29 -0700 Subject: [PATCH 165/455] add tests for including permittivity in EM simulations --- tests/em/fdem/forward/test_permittivity.py | 183 +++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/em/fdem/forward/test_permittivity.py diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py new file mode 100644 index 0000000000..3766a0628a --- /dev/null +++ b/tests/em/fdem/forward/test_permittivity.py @@ -0,0 +1,183 @@ +import pytest + +import numpy as np +from scipy.constants import epsilon_0 + +import geoana +import discretize +from discretize import utils +from SimPEG.electromagnetics import frequency_domain as fdem +from SimPEG.electromagnetics import resistivity as dc +from SimPEG import utils, maps, Report +from pymatsolver import Pardiso + + +# set up the mesh +hx = 1 +hz = 1 +nx = 50 +nz = int(2 * nx) + 1 +npad = 20 +pf = 1.3 + +mesh = discretize.CylindricalMesh( + [[(hx, nx), (hx, npad, pf)], 1, [(hz, npad, -pf), (hz, nz), (hz, npad, pf)]], + x0="00C", +) + +sigma = 1e-2 +conductivity = sigma * np.ones(mesh.n_cells) +epsilon_r_list = [0, 1, 1e3, 1e4, 1e5, 1e6] +epsilon_list = [epsilon_0 * e_r for e_r in epsilon_r_list] +frequency_list = [50, 100] + +threshold = 1e-1 + + +def get_inds(val, x, z): + if len(val) == mesh.n_faces: + grid = mesh.faces + elif len(val) == mesh.n_edges: + grid = mesh.edges + + return ( + (grid[:, 0] > x.min()) + & (grid[:, 0] < x.max()) + & (grid[:, 2] > z.min()) + & (grid[:, 2] < z.max()) + ) + + +def print_comparison( + numeric, analytic, x=np.r_[50, 100], z=np.r_[-100, 100], threshold=threshold +): + inds = get_inds(numeric, x, z) + results = [] + for component in ["real", "imag"]: + numeric_norm = np.linalg.norm(np.abs(getattr(numeric[inds], component))) + analytic_norm = np.linalg.norm(np.abs(getattr(analytic[inds], component))) + difference = np.linalg.norm( + np.abs( + getattr(analytic[inds], component) - getattr(numeric[inds], component) + ) + ) + print(f"{component} numeric : {numeric_norm:1.4e}") + print(f"{component} analytic : {analytic_norm:1.4e}") + print(f"{component} difference : {difference:1.4e}\n") + results.append(difference / np.mean([numeric_norm, analytic_norm]) < threshold) + print(results) + return results + + +@pytest.mark.parametrize("epsilon", epsilon_list) +@pytest.mark.parametrize("frequency", frequency_list) +@pytest.mark.parametrize( + "simulation", + [ + lambda survey, epsilon: fdem.Simulation3DElectricField( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + lambda survey, epsilon: fdem.Simulation3DMagneticFluxDensity( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + ], +) +def test_mag_dipole(epsilon, frequency, simulation): + sources = [fdem.sources.MagDipole([], frequency, location=np.r_[0, 0, 0])] + survey = fdem.Survey(sources) + sim = simulation(survey, epsilon) + fields = sim.fields() + + analytic_bdipole = geoana.em.fdem.MagneticDipoleWholeSpace( + sigma=sigma, epsilon=epsilon, frequency=frequency, orientation="Z" + ) + analytics = { + "b": np.hstack( + [ + analytic_bdipole.magnetic_flux_density(mesh.faces_x)[:, 0], + analytic_bdipole.magnetic_flux_density(mesh.faces_z)[:, 2], + ] + ), + "e": analytic_bdipole.electric_field(mesh.edges_y)[:, 1], + "h": np.hstack( + [ + analytic_bdipole.magnetic_field(mesh.faces_x)[:, 0], + analytic_bdipole.magnetic_field(mesh.faces_z)[:, 2], + ] + ), + "j": analytic_bdipole.current_density(mesh.edges_y)[:, 1], + } + + for f, analytic in analytics.items(): + print(f"Testing Mag dipole: {f}") + test = print_comparison(fields[:, f].squeeze(), analytic) + assert np.all(test) + + +@pytest.mark.parametrize("epsilon", epsilon_list) +@pytest.mark.parametrize("frequency", frequency_list) +@pytest.mark.parametrize( + "simulation", + [ + lambda survey, epsilon: fdem.Simulation3DCurrentDensity( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + lambda survey, epsilon: fdem.Simulation3DMagneticField( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + ], +) +def test_e_dipole(epsilon, frequency, simulation): + sources = [ + fdem.sources.LineCurrent( + [], frequency, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + ] + survey = fdem.Survey(sources) + sim = simulation(survey, epsilon) + fields = sim.fields() + + analytic_edipole = geoana.em.fdem.ElectricDipoleWholeSpace( + sigma=sigma, epsilon=epsilon, frequency=frequency, orientation="Z" + ) + analytics = { + "j": np.hstack( + [ + analytic_edipole.current_density(mesh.faces_x)[:, 0], + analytic_edipole.current_density(mesh.faces_z)[:, 2], + ] + ), + "h": analytic_edipole.magnetic_field(mesh.edges_y)[:, 1], + "e": np.hstack( + [ + analytic_edipole.electric_field(mesh.faces_x)[:, 0], + analytic_edipole.electric_field(mesh.faces_z)[:, 2], + ] + ), + "b": analytic_edipole.magnetic_flux_density(mesh.edges_y)[:, 1], + } + + for f, analytic in analytics.items(): + print(f"Testing E dipole: {f}") + test = print_comparison(fields[:, f].squeeze(), analytic) + assert np.all(test) From 88acb831e739832f8e0d3044974f3fba355974a3 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 10:24:20 -0700 Subject: [PATCH 166/455] pipelines syntax --- .azure-pipelines/matrix.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 24b61baf83..7910f7a458 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -2,18 +2,8 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] test: [ -# 'tests/em', -# 'tests/base tests/flow tests/seis tests/utils tests/meta', -# 'tests/docs', -# 'tests/examples/test_examples_1.py', -# 'tests/examples/test_examples_2.py', -# 'tests/examples/test_examples_3.py', -# 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', -# 'tests/examples/test_tutorials_3.py', -# 'tests/pf', -# 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. - '-s -v tests/meta' - ] + 'tests/meta -s -v' + ] jobs: - ${{ each os in parameters.os }}: From 5ec66091550e797870ec43c8e3ed3a424ff039d5 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 11:10:20 -0700 Subject: [PATCH 167/455] conditionally allow invertible property to also be optional --- SimPEG/props.py | 28 ++-- tests/base/test_Props.py | 283 +++++++++++++++++++++------------------ 2 files changed, 172 insertions(+), 139 deletions(-) diff --git a/SimPEG/props.py b/SimPEG/props.py index c5e0e3b64a..8732b012d4 100644 --- a/SimPEG/props.py +++ b/SimPEG/props.py @@ -68,16 +68,23 @@ class PhysicalProperty: reciprocal = None def __init__( - self, short_details, mapping=None, shape=None, default=None, dtype=None + self, + short_details, + mapping=None, + shape=None, + default=None, + dtype=None, + optional=False, ): self.short_details = short_details if mapping is not None: mapping.prop = self self._mapping = mapping + self.optional = optional self.shape = shape - self.dtype = None + self.dtype = dtype @property def name(self): @@ -109,6 +116,8 @@ def get_property(scope): shape_str = "" else: shape_str = f"{scope.shape} " + if scope.optional: + shape_str = f"None or {shape_str}" dtype_str = f" of {scope.dtype}" if scope.dtype is None: dtype_str = "" @@ -130,7 +139,7 @@ def fget(self): return 1.0 / value # If I don't have a mapping if scope.mapping is None: - # I done have a reciprocal, or it doesn't have a mapping + # I dont have a reciprocal, or it doesn't have a mapping if scope.reciprocal is None: return None if scope.reciprocal.mapping is None: @@ -142,14 +151,16 @@ def fget(self): ) ) # Set by mapped reciprocal - print("returning this thing?") return 1.0 / getattr(self, scope.reciprocal.name) mapping = getattr(self, scope.mapping.name, None) if mapping is None: - raise AttributeError( - f"Neither a value for `{scope.name}` or mapping for `{scope.mapping.name}` has not been set." - ) + if scope.optional: + return None + else: + raise AttributeError( + f"Neither a value for `{scope.name}` or mapping for `{scope.mapping.name}` has not been set." + ) if self.model is None: raise AttributeError( f"A `model` is required for physical property {scope.name}" @@ -238,12 +249,13 @@ def fdel(self): return property(fget=fget, fset=fset, fdel=fdel, doc=doc) -def Invertible(property_name): +def Invertible(property_name, optional=False): mapping = Mapping(f"Mapping of the inversion model to {property_name}.") physical_property = PhysicalProperty( f"{property_name.capitalize()} physical property model.", mapping=mapping, + optional=optional, ) property_derivative = Derivative( diff --git a/tests/base/test_Props.py b/tests/base/test_Props.py index ff78a50dc1..6b718e2f4a 100644 --- a/tests/base/test_Props.py +++ b/tests/base/test_Props.py @@ -1,6 +1,7 @@ import unittest import numpy as np import inspect +import pytest import discretize @@ -124,137 +125,157 @@ def __init__(self, nest_model=None, **kwargs): self.nest_model = nest_model -class TestPropMaps(unittest.TestCase): - def setUp(self): - pass - - def test_basic(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - assert expMap.nP == 3 - - for Example in [SimpleExample, ShortcutExample]: - PM = Example(sigmaMap=expMap) - assert PM.sigmaMap is not None - assert PM.sigmaMap is expMap - - # There is currently no model, so sigma, which is mapped, fails - self.assertRaises(AttributeError, getattr, PM, "sigma") - - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - # PM = pickle.loads(pickle.dumps(PM)) - # PM = maps.ExpMap.deserialize(PM.serialize()) - - assert np.all( - PM.sigmaDeriv.todense() - == utils.sdiag(np.exp(np.r_[1.0, 2.0, 3.0])).todense() - ) - - # If we set sigma, we should delete the mapping - PM.sigma = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - # PM = pickle.loads(pickle.dumps(PM)) - assert PM.sigmaMap is None - assert PM.sigmaDeriv == 0 - - del PM.model - # sigma is not changed - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - - def test_reciprocal(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - - PM = ReciprocalMappingExample() - - self.assertRaises(AttributeError, getattr, PM, "sigma") - PM.sigmaMap = expMap - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert PM.rhoMap is None - assert PM.sigmaMap is None - assert PM.rhoDeriv == 0 - assert PM.sigmaDeriv == 0 - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.sigmaMap = expMap - # change your mind? - # PM = pickle.loads(pickle.dumps(PM)) - PM.rhoMap = expMap - assert PM._sigmaMap is None - assert len(PM.rhoMap) == 1 - assert len(PM.sigmaMap) == 2 - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.sigma == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - # PM = pickle.loads(pickle.dumps(PM)) - assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) - - def test_reciprocal_no_map(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - - PM = ReciprocalExample() - self.assertRaises(AttributeError, getattr, PM, "sigma") - - PM.sigmaMap = expMap - # PM = pickle.loads(pickle.dumps(PM)) - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert PM.sigmaMap is None - assert PM.sigmaDeriv == 0 - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.sigmaMap = expMap - assert len(PM.sigmaMap) == 1 - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) - - def test_reciprocal_no_maps(self): - PM = ReciprocalPropExample() - self.assertRaises(AttributeError, getattr, PM, "sigma") - - # PM = pickle.loads(pickle.dumps(PM)) - PM.sigma = np.r_[1.0, 2.0, 3.0] - # PM = pickle.loads(pickle.dumps(PM)) - - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - def test_reciprocal_defaults(self): - PM = ReciprocalPropExampleDefaults() - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) - - rho = np.r_[2.0, 4.0, 6.0] - PM.rho = rho - assert np.all(PM.rho == rho) - assert np.all(PM.sigma == 1.0 / rho) - - def test_multi_parameter_inversion(self): - """The setup of the defaults should not invalidated the - mappings or other defaults. - """ - PM = ComplicatedInversion() - params = inspect.signature(ComplicatedInversion).parameters - - np.testing.assert_equal(PM.Ks, params["Ks"].default) - np.testing.assert_equal(PM.gamma, params["gamma"].default) - np.testing.assert_equal(PM.A, params["A"].default) - - def test_nested(self): - PM = NestedModels() - assert PM._has_nested_models is True +class OptionalInvertible(props.HasModel): + sigma, sigmaMap, sigmaDeriv = props.Invertible( + "Electrical conductivity (S/m)", optional=True + ) + + +@pytest.mark.parametrize("example", [SimpleExample, ShortcutExample]) +def test_basic(example): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + assert expMap.nP == 3 + + PM = example(sigmaMap=expMap) + assert PM.sigmaMap is not None + assert PM.sigmaMap is expMap + + # There is currently no model, so sigma, which is mapped, fails + with pytest.raises(AttributeError): + PM.sigma + + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + # PM = pickle.loads(pickle.dumps(PM)) + # PM = maps.ExpMap.deserialize(PM.serialize()) + + assert np.all( + PM.sigmaDeriv.todense() == utils.sdiag(np.exp(np.r_[1.0, 2.0, 3.0])).todense() + ) + + # If we set sigma, we should delete the mapping + PM.sigma = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + # PM = pickle.loads(pickle.dumps(PM)) + assert PM.sigmaMap is None + assert PM.sigmaDeriv == 0 + + del PM.model + # sigma is not changed + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + + +def test_reciprocal(): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + + PM = ReciprocalMappingExample() + + with pytest.raises(AttributeError): + PM.sigma + PM.sigmaMap = expMap + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert PM.rhoMap is None + assert PM.sigmaMap is None + assert PM.rhoDeriv == 0 + assert PM.sigmaDeriv == 0 + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.sigmaMap = expMap + # change your mind? + # PM = pickle.loads(pickle.dumps(PM)) + PM.rhoMap = expMap + assert PM._sigmaMap is None + assert len(PM.rhoMap) == 1 + assert len(PM.sigmaMap) == 2 + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.sigma == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + # PM = pickle.loads(pickle.dumps(PM)) + assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) + + +def test_reciprocal_no_map(): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + + PM = ReciprocalExample() + with pytest.raises(AttributeError): + PM.sigma + + PM.sigmaMap = expMap + # PM = pickle.loads(pickle.dumps(PM)) + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert PM.sigmaMap is None + assert PM.sigmaDeriv == 0 + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.sigmaMap = expMap + assert len(PM.sigmaMap) == 1 + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) + + +def test_reciprocal_no_maps(): + PM = ReciprocalPropExample() + with pytest.raises(AttributeError): + PM.sigma + + # PM = pickle.loads(pickle.dumps(PM)) + PM.sigma = np.r_[1.0, 2.0, 3.0] + # PM = pickle.loads(pickle.dumps(PM)) + + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + +def test_reciprocal_defaults(): + PM = ReciprocalPropExampleDefaults() + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) + + rho = np.r_[2.0, 4.0, 6.0] + PM.rho = rho + assert np.all(PM.rho == rho) + assert np.all(PM.sigma == 1.0 / rho) + + +def test_multi_parameter_inversion(): + """The setup of the defaults should not invalidated the + mappings or other defaults. + """ + PM = ComplicatedInversion() + params = inspect.signature(ComplicatedInversion).parameters + + np.testing.assert_equal(PM.Ks, params["Ks"].default) + np.testing.assert_equal(PM.gamma, params["gamma"].default) + np.testing.assert_equal(PM.A, params["A"].default) + + +def test_nested(): + PM = NestedModels() + assert PM._has_nested_models is True + + +def test_optional_inverted(): + modeler = OptionalInvertible() + assert modeler.sigmaMap is None + assert modeler.sigma is None + + modeler.sigma = 10 + assert modeler.sigma == 10 if __name__ == "__main__": From c23e184aec50319c8f135a3c60ffa914f2c2cb1b Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 11:28:19 -0700 Subject: [PATCH 168/455] syntax --- .azure-pipelines/matrix.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 7910f7a458..05b0d23ae3 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -1,9 +1,7 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] - test: [ - 'tests/meta -s -v' - ] + test: ['tests/meta -s -v'] jobs: - ${{ each os in parameters.os }}: From 056c5a682a4a282c89be190526546b009094768f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 13:28:49 -0700 Subject: [PATCH 169/455] temporary change --- .azure-pipelines/matrix.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 8f5afb8cb6..984ea3e4ac 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -1,17 +1,7 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] - test: ['tests/em', - 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs -s -v', - 'tests/examples/test_examples_1.py', - 'tests/examples/test_examples_2.py', - 'tests/examples/test_examples_3.py', - 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', - 'tests/examples/test_tutorials_3.py', - 'tests/pf', - 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. - ] + test: ['tests/meta'] jobs: - ${{ each os in parameters.os }}: @@ -21,7 +11,7 @@ jobs: displayName: ${{ os }}_${{ py_vers }}_${{ test }} pool: vmImage: ${{ os }} - timeoutInMinutes: 120 + timeoutInMinutes: 30 steps: - script: | wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" From 9f5e96c7641eb670282f9dedd1576522b15e171a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 13:36:43 -0700 Subject: [PATCH 170/455] try to identify location of hang --- .azure-pipelines/matrix.yml | 2 +- tests/meta/test_multiprocessing_sim.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 984ea3e4ac..b1d59ce0e5 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -37,7 +37,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test export KMP_WARNINGS=0 - pytest ${{ test }} --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning + pytest ${{ test }} -s -v --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning displayName: 'Testing ${{ test }}' - script: | diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 5f393c7b73..ea8e615124 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -210,30 +210,36 @@ def test_repeat_correctness(): try: # test field things + print("test 1") f_serial = serial_sim.fields(t_model) f_parallel = parallel_sim.fields(t_model) # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) + print("test 2") d_full = serial_sim.dpred(t_model, f_serial) d_repeat = parallel_sim.dpred(t_model, f_parallel) np.testing.assert_equal(d_full, d_repeat) + print("test 3") # test Jvec u = np.random.rand(len(t_model)) jvec_full = serial_sim.Jvec(t_model, u, f=f_serial) jvec_mult = parallel_sim.Jvec(t_model, u, f=f_parallel) np.testing.assert_allclose(jvec_full, jvec_mult) + print("test 4") # test Jtvec v = np.random.rand(len(sim_ts) * survey.nD) jtvec_full = serial_sim.Jtvec(t_model, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(t_model, v, f=f_parallel) np.testing.assert_allclose(jtvec_full, jtvec_mult) + print("test 5") # test get diag diag_full = serial_sim.getJtJdiag(t_model, f=f_serial) diag_mult = parallel_sim.getJtJdiag(t_model, f=f_parallel) np.testing.assert_allclose(diag_full, diag_mult) + print("test 6") except Exception as err: raise err finally: From 8f57ad8fff7792f254bffec5c075573ffbeb13a7 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 8 Jun 2023 13:51:35 -0700 Subject: [PATCH 171/455] add some more debug printing --- tests/meta/test_multiprocessing_sim.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index ea8e615124..f6de4d86ca 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -51,34 +51,40 @@ def test_meta_correctness(): serial_sim = MetaSimulation(dc_sims, dc_mappings) parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) + print("In test multi sim") try: # create fields objects + print("test A") f_serial = serial_sim.fields(m_test) f_parallel = parallel_sim.fields(m_test) + print("test B") # test data output d_full = serial_sim.dpred(m_test, f=f_serial) d_mult = parallel_sim.dpred(m_test, f=f_parallel) np.testing.assert_allclose(d_full, d_mult) + print("test C") # test Jvec u = np.random.rand(mesh.n_cells) jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) - + print("test D") np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec v = np.random.rand(serial_sim.survey.nD) jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + print("test E") np.testing.assert_allclose(jtvec_full, jtvec_mult) # test get diag diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + print("test F") np.testing.assert_allclose(diag_full, diag_mult) @@ -86,18 +92,22 @@ def test_meta_correctness(): parallel_sim.model = m_test d_mult2 = parallel_sim.dpred() np.testing.assert_allclose(d_mult, d_mult2) + print("test g") jvec_mult2 = parallel_sim.Jvec(m_test, u) np.testing.assert_allclose(jvec_mult, jvec_mult2) + print("test h") jtvec_mult2 = parallel_sim.Jtvec(m_test, v) np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + print("test i") # also pass a diagonal matrix here for testing. parallel_sim._jtjdiag = None W = sp.eye(parallel_sim.survey.nD) diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) np.testing.assert_allclose(diag_mult, diag_mult2) + print("test j") except Exception as err: raise err finally: From 83e87861bf703fbd87b04041772dc556ccee8771 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 8 Jun 2023 16:52:22 -0700 Subject: [PATCH 172/455] Catch indActive argument in BaseRegularization --- SimPEG/regularization/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index caa304c380..4bdf36ab41 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -58,6 +58,19 @@ def __init__( self._regularization_mesh = mesh self._weights = {} + if (key := "indActive") in kwargs: + if active_cells is not None: + raise ValueError( + f"Cannot simultanously pass 'active_cells' and '{key}'. " + "Pass 'active_cells' only." + ) + warnings.warn( + f"The '{key}' argument has been deprecated, please use 'active_cells'. " + "It will be removed in future versions of SimPEG.", + DeprecationWarning, + ) + active_cells = kwargs.pop(key) + if active_cells is not None: self.active_cells = active_cells From 5bb48766bdd814a5d70e858f1cee134aabb2302d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 8 Jun 2023 16:52:58 -0700 Subject: [PATCH 173/455] Remove kwargs from BaseObjectiveFunction and Combo --- SimPEG/objective_function.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 50c047d8a5..3f568f6b58 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -30,14 +30,12 @@ def __init__( has_fields=False, counter=None, debug=False, - **kwargs, ): self._nP = nP self._mapping = mapping self.counter = counter self.debug = debug self.has_fields = has_fields - set_kwargs(self, **kwargs) def __call__(self, x, f=None): """ @@ -268,7 +266,7 @@ class ComboObjectiveFunction(BaseObjectiveFunction): _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True, **kwargs): + def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): # Define default lists if None if objfcts is None: objfcts = [] @@ -287,7 +285,7 @@ def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True, **kwargs) else: nP = None - super().__init__(nP=nP, **kwargs) + super().__init__(nP=nP) self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add From cbf814d78be5da079b4cb94d96b80e603c7b5610 Mon Sep 17 00:00:00 2001 From: dccowan Date: Thu, 8 Jun 2023 18:32:06 -0700 Subject: [PATCH 174/455] Vector, PGI smallness, linear correspondence regularization --- SimPEG/regularization/base.py | 2 +- SimPEG/regularization/correspondence.py | 165 +++++++++++++++++------ SimPEG/regularization/pgi.py | 167 ++++++++++++++++++++++-- SimPEG/regularization/vector.py | 51 ++++---- 4 files changed, 308 insertions(+), 77 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 7b926102c3..7ea82d3e2e 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -2175,7 +2175,7 @@ class BaseSimilarityMeasure(BaseRegularization): the mesh on which the simulation is defined. wire_map : SimPEG.maps.WireMap Wire map connecting physical properties defined on active cells of the - :class:`RegularizationMesh`` to the entire model. + :class:`RegularizationMesh` to the entire model. """ def __init__(self, mesh, wire_map, **kwargs): diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index e94c0efe82..a9fcec8af0 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -7,15 +7,40 @@ class LinearCorrespondence(BaseSimilarityMeasure): - r""" - The petrophysical linear constraint for joint inversions. - - ..math:: - - \phi_c({\mathbf m}_{\mathbf1},{\mathbf m}_{\mathbf2}) - = \lambda\sum_{i=1}^M (k_1*m_1 + k_2*m_2 + k_3) - - Assuming that we are working with two models only. + r"""Linear correspondence regularization for joint inversions with two physical properties. + + ``Linear correspondence`` regularization is used to impose a constraint on the linear + combination of two physical properties. + + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : SimPEG.maps.Wires + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + coefficients : None, (3) numpy.ndarray of float + Coefficients :math:`\{ \lambda_1, \lambda_2, \lambda_3 \}` for the linear relationship + between model parameters. If ``None``, the coefficients are set to + :math:`\{ 1, -1, 0 \}`. + + Notes + ----- + Let :math:`\mathbf{m}` be a discrete model consisting of two physical property types such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + Where :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` define the coefficients for the linear + correspondence vector, the regularization function (objective function) is given by: + + .. math:: + \phi (\mathbf{m}) + = \frac{1}{2} \big \| \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 \big \|^2 """ @@ -27,11 +52,12 @@ def __init__(self, mesh, wire_map, coefficients=None, **kwargs): @property def coefficients(self): - """coefficients for the linear relationship between parameters. + """Coefficients for the linear relationship between model parameters. Returns ------- (3) numpy.ndarray of float + Coefficients for the linear relationship between model parameters. """ return self._coefficients @@ -42,21 +68,28 @@ def coefficients(self, value): ) def relation(self, model): - """ - Computes the values of petrophysical linear relationship between two different - geophysical models. + r"""Computes the linear correspondence vector for the model provided. + + For a model consisting of two physical properties such that: - The linear relationship is defined as: + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - f(m1, m2) = k1*m1 + k2*m2 + k3 + this method computer the linear correspondence vector for coefficients + :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` as follows: - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] + .. math:: + \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 - :rtype: float - :return: linearly related petrophysical values of two different models, - dimension: M by 1, :M number of model parameters. + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model for which the linear correspondence vector is evaluated. + Returns + ------- + float + The linear correspondence vector for the model provided. """ m1, m2 = self.wire_map * model k1, k2, k3 = self.coefficients @@ -64,28 +97,48 @@ def relation(self, model): return k1 * m1 + k2 * m2 + k3 def __call__(self, model): - """ - Computes the sum of values of petrophysical linear relationship - between two different geophysical models. + """Evaluate the regularization function for the model provided. - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model for which the function is evaluated. - :rtype: float - :return: a scalar value. + Returns + ------- + float + The regularization function evaluated for the model provided. """ result = self.relation(model) return 0.5 * result.T @ result def deriv(self, model): - """Computes the Jacobian of the coupling term. + r"""Jacobian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + For a model :math:`\mathbf{m}` consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Jacobian has the form: - :param list of numpy.ndarray ind_models: [model1, model2,...] + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} - :rtype: numpy.ndarray - :return: result: gradient of the coupling term with respect to model1, model2, - :dimension 2M by 1, :M number of model parameters. + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + (n_param, ) numpy.ndarray + Jacobian of the regularization function evaluated for the model provided. """ k1, k2, k3 = self.coefficients r = self.relation(model) @@ -97,15 +150,45 @@ def deriv(self, model): return result def deriv2(self, model, v=None): - """Computes the Hessian of the linear coupling term. - - :param list of numpy.ndarray ind_models: [model1, model2, ...] - :param numpy.ndarray v: vector to be multiplied by Hessian - :rtype: scipy.sparse.csr_matrix if v is None - numpy.ndarray if v is not None - :return Hessian matrix: | h11, h21 | :dimension 2M*2M. - | | - | h12, h22 | + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: + For a model :math:`\mathbf{m}` consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + v : None, (n_param, ) numpy.ndarray (optional) + A numpy array to model the Hessian by. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian + for the models provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. """ k1, k2, k3 = self.coefficients diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index fc38cb0e3d..7cdbbda157 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -31,18 +31,163 @@ class PGIsmallness(Smallness): - """ - Smallness term for the petrophysically constrained regularization (PGI) - with cell_weights similar to the regularization.tikhonov.SimpleSmall class. - - PARAMETERS + r"""Smallness regularization function for petrophysically guided inversion (PGI). + + ``PGIsmallness`` is used to recover models in which the physical property values are + consistent with petrophysical information. ``PGIsmallness`` regularization assumes that + the statistical distribution of physical property values defining the model is characterized + by a Gaussian mixture model (GMM). That is, the physical property values for each specified + geological unit are characterized by a separate multivariate Gaussian distribution, + which are summed to define the GMM. ``PGIsmallness`` is generally combined with other + regularization classes to form a complete regularization for the inverse problem; see + :class:`PGI`. + + ``PGIsmallness`` can be implemented to invert for a single physical property or multiple + physical properties, each of which are defined on a linear scale (e.g. density) or a log-scale + (e.g. electrical conductivity). If the statistical distribution(s) of physical property values + for each property type are known, the GMM can be constructed and left static throughout the + inversion. Otherwise, the recovered model at each iteration is used to update the GMM. + And the updated GMM is used to constrain the recovered model for the following iteration. + + Parameters ---------- - :param SimPEG.utils.WeightedGaussianMixture gmm: GMM to use - :param SimPEG.maps.Wires wiresmap: wires mapping to the various physical properties - :param list maplist: list of SimPEG.maps for each physical property. - :param discretize.BaseMesh mesh: tensor, QuadTree or Octree mesh - :param boolean approx_gradient: use the L2-approximation of the gradient, default is True - :param boolean approx_eval: use the L2-approximation evaluation of the smallness term + gmmref : SimPEG.utils.WeightedGaussianMixture + Reference Gaussian mixture model. + gmm : None, SimPEG.utils.WeightedGaussianMixture + Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the + inversion. If set, the Gaussian mixture model used to constrain the recovered model is + static throughout the inversion. + wiresmap : None, SimPEG.maps.Wires + Defines the mapping from the entire model to the parameters of each type. + If ``None``, we assume only a single physical property type in the inversion. + maplist : None, list of SimPEG.maps + List of mappings from model values to physical property values. + One for each physical property. If ``None``, we assume a single physical property type + in the regularization and an :class:`.maps.IdentityMap` from model values to physical + property values. + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. Implemented for + `tensor`, `QuadTree` or `Octree` meshes. + approx_gradient : bool + If ``True``, use the L2-approximation of the gradient by assuming + physical property values of different types are uncorrelated. + approx_eval : bool + If ``True``, use the L2-approximation evaluation of the smallness term by assuming + physical property values of different types are uncorrelated. + approx_hessian : bool + Approximate the Hessian of the regularization function. + non_linear_relationship : bool + Elaborate. + + Notes + ----- + For one or more physical property types (e.g. conductivity, density, susceptibility), + the ``PGIsmallness`` regularization function (objective function) is derived by setting a + Gaussian mixture model (GMM) as the prior within a Baysian inversion scheme. + For a comprehensive description, see + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + We let :math:`\Theta` store all of the means (:math:`\boldsymbol{\mu}`), covariances + (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) + defining the GMM. And let :math:`\mathbf{z}^\ast` define an indexing array that + extracts the GMM parameters for the most representative rock unit within each active cell + in the :class:`RegularizationMesh`. The regularization function (objective function) for + ``PGIsmallness`` is given by: + + .. math:: + \phi (\mathbf{m}) = \frac{1}{2} + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ]^T + \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast )` is the reference model, and + - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is a weighting matrix. + + ``PGIsmallness`` regularization can be used for models consisting of one or more physical + property types. The ordering of the physical property types within the model is defined + using the `wiresmap`. And the mapping from model parameter values to physical property + values is specified with `maplist`. For :math:`K` physical property types, the model is + an array vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} + + When the `approx_eval` property is ``True``, we assume the physical property types have + values that are uncorrelated. In this case, the weighting matrix is diagonal and the + regularization function (objective function) can be expressed as: + + .. math:: + \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 + + When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property + to ``True`` so that the least-squares approximation is used to compute the gradient. + + **Constructing the Reference Model and Weighting Matrix:** + + The reference model used in the regularization function is constructed by extracting the means + :math:`\boldsymbol{\mu}` from the GMM using the indexing array :math:`\mathbf{z}^\ast`. + We represent this vector as: + + .. math:: + \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} + + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances + :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: + + .. math:: + \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, + diag \big ( \mathbf{v \odot w} \big ) + + where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` + are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal + covariances are zero and we can use a weighting matrix of the form: + + .. math:: + \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} + \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) + + where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the + indexing array :math:`\mathbf{z}^\ast`. + + **Updating the Gaussian Mixture Model:** + + When the GMM is set using the `gmm` property, the GMM remains static throughout the inversion. + When the `gmm` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the `gmmref` property is not completely representative + of the physical property distributions for each rock unit, and we update the all of the means + (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants + (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: + + .. math:: + \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) + + using a MAP variation of the expectation-maximization clustering algorithm introduced in + Dempster (et al. 1977). + + **Updating the Indexing Array:** + + As the model (and GMM) are updated throughout the inversion, the rock unit considered most + indicative of the geology within each cell is updated; which is represented by the indexing + array :math:`\mathbf{z}^\ast`. W. For the current GMM with means (:math:`\boldsymbol{\mu}`), + covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`), + we solve the following for each cell: + + .. math:: + z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) + + where + + - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, + - :math:`\gamma_{i,n}` is the proportion for cell :math:`i` and rock unit :math:`n` + - :math:`\boldsymbol{\mu}_n` are the mean property values for unit :math:`n`, + - :math:`\boldsymbol{\Sigma}_n` are the covariances for unit :math:`n`, and + - :math:`\mathcal{N}` represent the multivariate Gaussian distribution. + """ _multiplier_pair = "alpha_pgi" diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 4192a325f8..ee2cdbab85 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -29,13 +29,17 @@ def _weights_shapes(self) -> list[tuple[int]]: class CrossReferenceRegularization(Smallness, BaseVectorRegularization): - r"""Cross reference regularization for inversion to recover vector quantities. - - This regularizer measures the magnitude of the cross product of the vector model - with a reference vector model. This encourages the vectors in the model to point - in the reference direction. The cross product of two vectors is minimized when they - are parallel (or anti-parallel) to each other, and maximized when the vectors are - perpendicular to each other. + r"""Cross reference regularization for models representing vector quantities. + + ``CrossReferenceRegularization`` encourages the vectors in the recovered model to + be oriented in the same directions as the vector in a reference vector model. + The regularization function (objective function) constrains the inversion by penalizing + the magnitude of the-cross product of the vector model with a reference vector model. + The cross product, and therefore the objective function, is minimized when vectors + in the model and reference vector model are parallel (or anti-parallel) to each other. + And it is maximized when the vectors are perpendicular to each other. + The reference vector model can be set using a single vector, or by defining a + vector for each mesh cell. Parameters ---------- @@ -79,12 +83,11 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): \phi (\vec{m}) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \cdot \, \Big [ \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big ]^2 - where :math:`\tilde{m}_i` are the model vectors at cell centers and + where :math:`\tilde{m}_i \in \mathbf{m}` are the model vectors at cell centers and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. - In practice, we frequently define the model :math:`\mathbf{m}` as a discrete - vector of the form: + In practice, the model is a discrete vector of the form: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} @@ -92,33 +95,30 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): where :math:`\mathbf{m_p}`, :math:`\mathbf{m_s}` and :math:`\mathbf{m_t}` represent vector components in the primary, secondary and tertiary directions at cell centers, respectively. The cross product between :math:`\mathbf{m}` and a similar reference vector - :math:`\mathbf{m^{ref}}` is a linear operation of the form: + :math:`\mathbf{m^{(ref)}}` (set with `ref_dir`) is a linear operation of the form: .. math:: - \mathbf{m} \times \mathbf{m^{ref}} = \mathbf{X m} + \mathbf{m} \times \mathbf{m^{ref}} = \mathbf{X m} = \begin{bmatrix} \mathbf{0} & -\boldsymbol{\Lambda_s} & \boldsymbol{\Lambda_t} \\ \boldsymbol{\Lambda_p} & \mathbf{0} & -\boldsymbol{\Lambda_t} \\ -\boldsymbol{\Lambda_p} & \boldsymbol{\Lambda_s} & \mathbf{0} - \end{bmatrix} + \end{bmatrix} \! \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} - where + where :math:`\mathbf{X}` is a linear operator that applies the cross-product on :math:`\mathbf{m}`, + :math:`\mathbf{W}` is the weighting matrix, and: - .. math: - \boldsymbol{\Lambda_j} = \textrm{diag} \Big ( \mathbf{m_j^{(red)}} \Big ) + .. math:: + \boldsymbol{\Lambda_j} = \textrm{diag} \Big ( \mathbf{m_j^{(ref)}} \Big ) \;\;\;\; \textrm{for} \; j=p,s,t - The discrete regularization function in linear form is given by: + The discrete regularization function in linear form can ultimately be expressed as: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W X m} \, \Big \|^2 - where - - - :math:`\boldsymbol{\Lambda}` applies the cross-products, and - - :math:`\mathbf{W}` is the weighting matrix. **Custom weights and the weighting matrix:** @@ -140,7 +140,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): - :math:`\otimes` is the Kronecker product, and - :math:`\mathbf{v}` are the cell volumes. - The weighting matrix used to apply the weights for smallness regularization is given by: + The weighting matrix used to apply the weights in the regularization function is given by: .. math:: \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) @@ -149,9 +149,12 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): ``numpy.ndarray``. The weights can be set all at once during instantiation with the `weights` keyword argument as follows: - >>> reg = Smallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + >>> reg = CrossReferenceRegularization( + >>> mesh, weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) - or set after instantiation using the `set_weights` method: + where `array_1` and `array_2` are (n_cells, dim) ``numpy.ndarray``. + Weights can also be set after instantiation using the `set_weights` method: >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) From a549fb391d390992f44531c85a7e8e0e6aef2d2a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 09:44:37 -0700 Subject: [PATCH 175/455] add some more print statements --- azure-pipelines.yml | 3 ++- tests/meta/test_multiprocessing_sim.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e49e9954b..16efd60982 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,6 +18,7 @@ stages: - stage: StyleChecks displayName: "Style Checks" + condition: False jobs: - job: displayName: Run style checks with Black @@ -59,7 +60,7 @@ stages: displayName: "Run flake8" - stage: Testing - dependsOn: StyleChecks + # dependsOn: StyleChecks jobs: - template: ./.azure-pipelines/matrix.yml diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index f6de4d86ca..7c069fdf1b 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -17,6 +17,7 @@ def test_meta_correctness(): + print("starting_meta_test_correct") mesh = TensorMesh([16, 16, 16], origin="CCN") rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] @@ -115,6 +116,7 @@ def test_meta_correctness(): def test_sum_correctness(): + print("starting_meta_test_sum") mesh = TensorMesh([16, 16, 16], origin="CCN") # Create gravity sum sims rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T @@ -196,6 +198,7 @@ def test_sum_correctness(): def test_repeat_correctness(): + print("starting_meta_test_repeat") mesh = TensorMesh([16, 16, 16], origin="CCN") rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T rx = gravity.Point(rx_locs, components=["gz"]) From 3c611a0eebd35c84c7b31f29effa1de38d63b45a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 09:47:54 -0700 Subject: [PATCH 176/455] add condition --- azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 16efd60982..57ba36ffe0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -60,7 +60,8 @@ stages: displayName: "Run flake8" - stage: Testing - # dependsOn: StyleChecks + dependsOn: StyleChecks + condition: True jobs: - template: ./.azure-pipelines/matrix.yml From 8d3c5943e6c03f326018e80d630e220b017bfa4c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 11:18:37 -0700 Subject: [PATCH 177/455] slightly more debug info --- SimPEG/meta/multiprocessing.py | 5 +++++ tests/meta/test_multiprocessing_sim.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index b0181baa39..c58a32327f 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -172,6 +172,7 @@ def __init__(self, simulations, mappings, n_processes=None): if n_processes is None: n_processes = cpu_count() + print(f"starting with {n_processes} processes") # split simulation,mappings up into chunks # (Which are currently defined using MetaSimulations) @@ -340,6 +341,10 @@ def __init__(self, simulation, mappings, n_processes=None): if n_processes is None: n_processes = cpu_count() + if n_processes is None: + n_processes = cpu_count() + print(f"starting with {n_processes} processes") + # split mappings up into chunks n_sim = len(mappings) chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 7c069fdf1b..ef79fe11c6 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -51,8 +51,9 @@ def test_meta_correctness(): dc_mappings.append(maps.IdentityMap()) serial_sim = MetaSimulation(dc_sims, dc_mappings) + print("created serial") parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) - print("In test multi sim") + print("created parallel") try: # create fields objects From 358eed5e83b7057485e5043e4a07a478c59514e3 Mon Sep 17 00:00:00 2001 From: dccowan Date: Fri, 9 Jun 2023 12:49:16 -0700 Subject: [PATCH 178/455] PGI and linear correspondence documentation --- SimPEG/regularization/base.py | 3 +- SimPEG/regularization/correspondence.py | 25 +- SimPEG/regularization/pgi.py | 461 ++++++++++++++++++++++-- 3 files changed, 452 insertions(+), 37 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 0dc5a004a0..cee042c3ca 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -459,7 +459,7 @@ def deriv(self, m) -> np.ndarray: Parameters ---------- - (n_param, ) numpy.ndarray + m : (n_param, ) numpy.ndarray The model for which the Jacobian is evaluated. Returns @@ -498,7 +498,6 @@ def deriv2(self, m, v=None) -> csr_matrix: If the input argument *v* is ``None``, the Hessian of the regularization function for the model provided is returned. If *v* is not ``None``, the Hessian multiplied by the vector provided is returned. - """ f_m_deriv = self.f_m_deriv(m) if v is None: diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index a9fcec8af0..ff2c8d9474 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -7,10 +7,12 @@ class LinearCorrespondence(BaseSimilarityMeasure): - r"""Linear correspondence regularization for joint inversions with two physical properties. + r"""Linear correspondence regularization for joint inversion with two physical properties. - ``Linear correspondence`` regularization is used to impose a constraint on the linear - combination of two physical properties. + ``LinearCorrespondence`` is used to recover a model where the differences between the model + parameter values for two physical property types are minimal. ``LinearCorrespondence`` + can also be used to minimize the squared L2-norm of a linear combination of model parameters + for two physical property types. Parameters ---------- @@ -35,13 +37,18 @@ class LinearCorrespondence(BaseSimilarityMeasure): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - Where :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` define the coefficients for the linear - correspondence vector, the regularization function (objective function) is given by: + Where :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` define scalar coefficients for a + linear combination of vectors :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}`, the regularization + function (objective function) is given by: .. math:: \phi (\mathbf{m}) = \frac{1}{2} \big \| \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 \big \|^2 + Scalar coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` are set using the + `coefficients` property. For a true linear correspondence constraint, we set + :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` to :math:`\{ 1, -1, 0 \}`. + """ def __init__(self, mesh, wire_map, coefficients=None, **kwargs): @@ -68,14 +75,14 @@ def coefficients(self, value): ) def relation(self, model): - r"""Computes the linear correspondence vector for the model provided. + r"""Computes the relation vector for the model provided. For a model consisting of two physical properties such that: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - this method computer the linear correspondence vector for coefficients + this method computer the relation vector for coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` as follows: .. math:: @@ -84,12 +91,12 @@ def relation(self, model): Parameters ---------- model : (n_param, ) numpy.ndarray - The model for which the linear correspondence vector is evaluated. + The model for which the relation vector is evaluated. Returns ------- float - The linear correspondence vector for the model provided. + The relation vector for the model provided. """ m1, m2 = self.wire_map * model k1, k2, k3 = self.coefficients diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index b8aecf1802..c3c40d50bf 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -58,11 +58,11 @@ class PGIsmallness(Smallness): inversion. If set, the Gaussian mixture model used to constrain the recovered model is static throughout the inversion. wiresmap : None, SimPEG.maps.Wires - Defines the mapping from the entire model to the parameters of each type. + Mapping from the model to the model parameters of each type. If ``None``, we assume only a single physical property type in the inversion. maplist : None, list of SimPEG.maps - List of mappings from model values to physical property values. - One for each physical property. If ``None``, we assume a single physical property type + Ordered list of mappings from model values to physical property values; + one for each physical property. If ``None``, we assume a single physical property type in the regularization and an :class:`.maps.IdentityMap` from model values to physical property values. mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh @@ -77,7 +77,7 @@ class PGIsmallness(Smallness): approx_hessian : bool Approximate the Hessian of the regularization function. non_linear_relationship : bool - Elaborate. + Whether relationships in the Gaussian mixture model are non-linear. Notes ----- @@ -90,7 +90,7 @@ class PGIsmallness(Smallness): We let :math:`\Theta` store all of the means (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) - defining the GMM. And let :math:`\mathbf{z}^\ast` define an indexing array that + defining the GMM. And let :math:`\mathbf{z}^\ast` define an membership array that extracts the GMM parameters for the most representative rock unit within each active cell in the :class:`RegularizationMesh`. The regularization function (objective function) for ``PGIsmallness`` is given by: @@ -130,7 +130,7 @@ class PGIsmallness(Smallness): **Constructing the Reference Model and Weighting Matrix:** The reference model used in the regularization function is constructed by extracting the means - :math:`\boldsymbol{\mu}` from the GMM using the indexing array :math:`\mathbf{z}^\ast`. + :math:`\boldsymbol{\mu}` from the GMM using the membership array :math:`\mathbf{z}^\ast`. We represent this vector as: .. math:: @@ -152,7 +152,7 @@ class PGIsmallness(Smallness): \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the - indexing array :math:`\mathbf{z}^\ast`. + membership array :math:`\mathbf{z}^\ast`. **Updating the Gaussian Mixture Model:** @@ -169,10 +169,10 @@ class PGIsmallness(Smallness): using a MAP variation of the expectation-maximization clustering algorithm introduced in Dempster (et al. 1977). - **Updating the Indexing Array:** + **Updating the Membership Array:** As the model (and GMM) are updated throughout the inversion, the rock unit considered most - indicative of the geology within each cell is updated; which is represented by the indexing + indicative of the geology within each cell is updated; which is represented by the membership array :math:`\mathbf{z}^\ast`. W. For the current GMM with means (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`), we solve the following for each cell: @@ -237,6 +237,14 @@ def __init__( self.set_weights(**weights) def set_weights(self, **weights): + """Adds (or updates) the specified weights. + + Parameters + ---------- + **weights : key, numpy.ndarray + Each keyword argument is added to the weights used the regularization object. + They can be accessed with their keyword argument. + """ for key, values in weights.items(): values = validate_ndarray_with_shape("weights", values, dtype=float) @@ -253,6 +261,15 @@ def set_weights(self, **weights): @property def gmm(self): + """Gaussian mixture model. + + Returns + ------- + None, SimPEG.utils.WeightedGaussianMixture + Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the + inversion. If set, the Gaussian mixture model used to constrain the recovered model is + static throughout the inversion. + """ if getattr(self, "_gmm", None) is None: self._gmm = copy.deepcopy(self.gmmref) return self._gmm @@ -264,15 +281,65 @@ def gmm(self, gm): @property def shape(self): - """""" + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. + """ return (self.wiresmap.nP,) def membership(self, m): + """Compute and return membership array for the model provided. + + The membership array stores the index of the rock unit most representative of each cell. + For a Gaussian mixture model containing the means and covariances for model parameter + types (physical property types) for all rock units, this method computes the membership + array for the model `m` provided. For a description of the membership array, see the + *Notes* section within the :class:`PGIsmallness` documentation. + + Parameters + ---------- + m : (nP, ) numpy.ndarray of float + The model. + + Returns + ------- + (n_active, ) numpy.ndarray of int + The membership array. + """ modellist = self.wiresmap * m model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T return self.gmm.predict(model) def compute_quasi_geology_model(self): + r"""Compute and return quasi geology model. + + For each active cell in the mesh, this method returns the mean values in the Gaussian + mixture model for the most representative rock unit, given the current model. See the + *Notes* section for a comprehensive description. + + Returns + ------- + (nP, ) numpy.ndarray + The quasi geology physical property model. + + Notes + ----- + Consider a Gaussian mixture model (GMM) for :math:`K` physical property types and + :math:`N` rock units. The mean model parameter values for rock unit + :math:`n \in \{ 1, \ldots , N \}` in the GMM is represented by a vector + :math:`\boldsymbol{\mu}_n` of length :math:`K`. For each active cell in the mesh, the + `compute_quasi_geology_model` method computes: + + .. math:: + g_i^ = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 + + where :math:`\mathbf{m}_i` are the model parameter values for cell :math:`i` for the + current model. The ordering of the output vector :math:`\mathbf{g}` is the same as the + model :math:`\mathbf{m}`. + """ # used once mref is built mreflist = self.wiresmap * self.reference_model mrefarray = np.c_[[a for a in mreflist]].T @@ -282,7 +349,13 @@ def compute_quasi_geology_model(self): @property def non_linear_relationships(self): - """Flag for non-linear GMM relationships""" + """Whether relationships in the Gaussian mixture model are non-linear. + + Returns + ------- + bool + Whether relationships in the Gaussian mixture model are non-linear. + """ return self._non_linear_relationships @non_linear_relationships.setter @@ -296,6 +369,13 @@ def non_linear_relationships(self, value: bool): @property def wiresmap(self): + """Mapping from the model to the model parameters of each type. + + Returns + ------- + SimPEG.maps.Wires + Mapping from the model to the model parameters of each type. + """ if getattr(self, "_wiresmap", None) is None: self._wiresmap = Wires(("m", self.regularization_mesh.nC)) return self._wiresmap @@ -314,6 +394,14 @@ def wiresmap(self, wires): @property def maplist(self): + """Ordered list of mappings from model values to physical property values. + + Returns + ------- + list of SimPEG.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. + """ if getattr(self, "_maplist", None) is None: self._maplist = [ IdentityMap(nP=self.regularization_mesh.nC) @@ -344,6 +432,20 @@ def maplist(self, maplist): @timeIt def __call__(self, m, external_weights=True): + """Evaluate the regularization function for the model provided. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + external_weights : bool + Include custom cell weighting when evaluating the regularization function. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ if external_weights: W = self.W else: @@ -421,6 +523,25 @@ def __call__(self, m, external_weights=True): @timeIt def deriv(self, m): + r"""Jacobian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + I.e.: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Jacobian is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The Jacobian of the regularization function evaluated for the model provided. + """ if getattr(self, "reference_model", None) is None: self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) @@ -609,6 +730,33 @@ def deriv(self, m): @timeIt def deriv2(self, m, v=None): + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the regularization + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ if getattr(self, "reference_model", None) is None: self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) @@ -814,17 +962,184 @@ def deriv2(self, m, v=None): class PGI(ComboObjectiveFunction): - """ - class similar to regularization.tikhonov.Simple, with a PGIsmallness. - PARAMETERS + r"""Regularization function for petrophysically guided inversion (PGI). + + ``PGI`` is used to recover models in which 1) the physical property values are consistent + with petrophysical information and 2) structures in the recovered model are geologically + plausible. ``PGI`` regularization is a weighted sum of :class:`PGIsmallness`, + :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` (optional) + regularization functions. The PGI smallness term assumes the statistical distribution of + physical property values defining the model is characterized + by a Gaussian mixture model (GMM). And the smoothness terms penalize large + spatial derivatives in the recovered model. + + ``PGI`` can be implemented to invert for a single physical property or multiple + physical properties, each of which are defined on a linear scale (e.g. density) or a log-scale + (e.g. electrical conductivity). If the statistical distribution(s) of physical property values + for each property type are known, the GMM can be constructed and left static throughout the + inversion. Otherwise, the recovered model at each iteration is used to update the GMM. + And the updated GMM is used to constrain the recovered model for the following iteration. + + Parameters ---------- - :param SimPEG.utils.WeightedGaussianMixture gmmref: refereence/prior GMM - :param SimPEG.utils.WeightedGaussianMixture gmm: GMM to use - :param SimPEG.maps.Wires wiresmap: wires mapping to the various physical properties - :param list maplist: list of SimPEG.maps for each physical property. - :param discretize.BaseMesh mesh: tensor, QuadTree or Octree mesh - :param boolean approx_gradient: use the L2-approximation of the gradient, default is True - :param boolean approx_eval: use the L2-approximation evaluation of the smallness term + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. Implemented for + `tensor`, `QuadTree` or `Octree` meshes. + gmmref : SimPEG.utils.WeightedGaussianMixture + Reference Gaussian mixture model. + gmm : None, SimPEG.utils.WeightedGaussianMixture + Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the + inversion. If set, the Gaussian mixture model used to constrain the recovered model is + static throughout the inversion. + alpha_pgi : float + Scaling constant for the PGI smallness term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + alpha_xx, alpha_yy, alpha_zz : 0, float + Scaling constants for the second order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + length scales; see :class:`regularization.WeightedLeastSquares`. + wiresmap : None, SimPEG.maps.Wires + Mapping from the model to the model parameters of each type. + If ``None``, we assume only a single physical property type in the inversion. + maplist : None, list of SimPEG.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. If ``None``, we assume a single physical property type + in the regularization and an :class:`.maps.IdentityMap` from model values to physical + property values. + approx_gradient : bool + If ``True``, use the L2-approximation of the gradient by assuming + physical property values of different types are uncorrelated. + approx_eval : bool + If ``True``, use the L2-approximation evaluation of the smallness term by assuming + physical property values of different types are uncorrelated. + approx_hessian : bool + Approximate the Hessian of the regularization function. + non_linear_relationship : bool + Whether relationships in the Gaussian mixture model are non-linear. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + + Notes + ----- + For one or more physical property types (e.g. conductivity, density, susceptibility), + the ``PGI`` regularization function (objective function) is derived by using a + Gaussian mixture model (GMM) to construct the prior within a Baysian + inversion scheme. For a comprehensive description, see + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + We let :math:`\Theta` store all of the means (:math:`\boldsymbol{\mu}`), covariances + (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) + defining the GMM. And let :math:`\mathbf{z}^\ast` define an membership array that + extracts the GMM parameters for the most representative rock unit within each active cell + in the :class:`RegularizationMesh`. The regularization function (objective function) for + ``PGI`` is given by: + + .. math:: + \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ]^T + \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \\ + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast )` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, + - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is the weighting matrix for PGI smallness, and + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices for smoothness terms. + + ``PGIsmallness`` regularization can be used for models consisting of one or more physical + property types. The ordering of the physical property types within the model is defined + using the `wiresmap`. And the mapping from model parameter values to physical property + values is specified with `maplist`. For :math:`K` physical property types, the model is + an array vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} + + When the `approx_eval` property is ``True``, we assume the physical property types have + values that are uncorrelated. In this case, the weighting matrix is diagonal and the + regularization function (objective function) can be expressed as: + + .. math:: + \phi (\mathbf{m}) &= \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property + to ``True`` so that the least-squares approximation is used to compute the gradient. + + **Constructing the Reference Model and Weighting Matrix:** + + The reference model used in the regularization function is constructed by extracting the means + :math:`\boldsymbol{\mu}` from the GMM using the membership array :math:`\mathbf{z}^\ast`. + We represent this vector as: + + .. math:: + \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} + + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances + :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: + + .. math:: + \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, + diag \big ( \mathbf{v \odot w} \big ) + + where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` + are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal + covariances are zero and we can use a weighting matrix of the form: + + .. math:: + \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} + \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) + + where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the + membership array :math:`\mathbf{z}^\ast`. + + **Updating the Gaussian Mixture Model:** + + When the GMM is set using the `gmm` property, the GMM remains static throughout the inversion. + When the `gmm` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the `gmmref` property is not completely representative + of the physical property distributions for each rock unit, and we update the all of the means + (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants + (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: + + .. math:: + \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) + + using a MAP variation of the expectation-maximization clustering algorithm introduced in + Dempster (et al. 1977). + + **Updating the Membership Array:** + + As the model (and GMM) are updated throughout the inversion, the rock unit considered most + indicative of the geology within each cell is updated; which is represented by the membership + array :math:`\mathbf{z}^\ast`. W. For the current GMM with means (:math:`\boldsymbol{\mu}`), + covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`), + we solve the following for each cell: + + .. math:: + z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) + + where + + - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, + - :math:`\gamma_{i,n}` is the proportion for cell :math:`i` and rock unit :math:`n` + - :math:`\boldsymbol{\mu}_n` are the mean property values for unit :math:`n`, + - :math:`\boldsymbol{\Sigma}_n` are the covariances for unit :math:`n`, and + - :math:`\mathcal{N}` represent the multivariate Gaussian distribution. + """ def __init__( @@ -899,7 +1214,13 @@ def __init__( @property def alpha_pgi(self): - """PGI smallness weight""" + """Scaling constant for the PGI smallness term. + + Returns + ------- + float + Scaling constant for the PGI smallness term. + """ if getattr(self, "_alpha_pgi", None) is None: self._alpha_pgi = self.multipliers[0] return self._alpha_pgi @@ -912,6 +1233,15 @@ def alpha_pgi(self, value): @property def gmm(self): + """Gaussian mixture model. + + Returns + ------- + None, SimPEG.utils.WeightedGaussianMixture + Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the + inversion. If set, the Gaussian mixture model used to constrain the recovered model is + static throughout the inversion. + """ return self.objfcts[0].gmm @gmm.setter @@ -919,19 +1249,78 @@ def gmm(self, gm): self.objfcts[0].gmm = copy.deepcopy(gm) def membership(self, m): + """Compute and return membership array for the model provided. + + The membership array stores the index of the rock unit most representative of each cell. + For a Gaussian mixture model containing the means and covariances for model parameter + types (physical property types) for all rock units, this method computes the membership + array for the model `m` provided. For a description of the membership array, see the + *Notes* section within the :class:`PGI` documentation. + + Parameters + ---------- + m : (nP, ) numpy.ndarray of float + The model. + + Returns + ------- + (n_active, ) numpy.ndarray of int + The membership array. + """ return self.objfcts[0].membership(m) def compute_quasi_geology_model(self): + r"""Compute and return quasi geology model. + + For each active cell in the mesh, this method returns the mean values in the Gaussian + mixture model for the most representative rock unit, given the current model. See the + *Notes* section for a comprehensive description. + + Returns + ------- + (nP, ) numpy.ndarray + The quasi geology physical property model. + + Notes + ----- + Consider a Gaussian mixture model (GMM) for :math:`K` physical property types and + :math:`N` rock units. The mean model parameter values for rock unit + :math:`n \in \{ 1, \ldots , N \}` in the GMM is represented by a vector + :math:`\boldsymbol{\mu}_n` of length :math:`K`. For each active cell in the mesh, the + `compute_quasi_geology_model` method computes: + + .. math:: + g_i^ = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 + + where :math:`\mathbf{m}_i` are the model parameter values for cell :math:`i` for the + current model. The ordering of the output vector :math:`\mathbf{g}` is the same as the + model :math:`\mathbf{m}`. + """ return self.objfcts[0].compute_quasi_geology_model() @property def wiresmap(self): + """Mapping from the model to the model parameters of each type. + + Returns + ------- + SimPEG.maps.Wires + Mapping from the model to the model parameters of each type. + """ if getattr(self, "_wiresmap", None) is None: self._wiresmap = Wires(("m", self.regularization_mesh.nC)) return self._wiresmap @property def maplist(self): + """Ordered list of mappings from model values to physical property values. + + Returns + ------- + list of SimPEG.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. + """ if getattr(self, "_maplist", None) is None: self._maplist = [ IdentityMap(nP=self.regularization_mesh.nC) @@ -941,7 +1330,16 @@ def maplist(self): @property def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" + """Regularization mesh. + + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. + + Returns + ------- + discretize.base.RegularizationMesh + Mesh on which the regularization is discretized. + """ return self._regularization_mesh @regularization_mesh.setter @@ -953,8 +1351,12 @@ def regularization_mesh(self, mesh: RegularizationMesh): @property def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. + """Whether to include the reference model in the smoothness objective functions. + + Returns + ------- + bool + Whether to include the reference model in the smoothness objective functions. """ return self._reference_model_in_smooth @@ -972,7 +1374,14 @@ def reference_model_in_smooth(self, value: bool): @property def reference_model(self) -> np.ndarray: - """Reference physical property model""" + """Reference model. + + Returns + ------- + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + """ return self.objfcts[0].reference_model @reference_model.setter From 994004e1b43e1f42c0265610320336179e8d7af8 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 14:12:47 -0700 Subject: [PATCH 179/455] add more debug --- SimPEG/meta/multiprocessing.py | 7 +++++++ tests/meta/test_multiprocessing_sim.py | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index c58a32327f..5dd6112288 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -177,14 +177,18 @@ def __init__(self, simulations, mappings, n_processes=None): # split simulation,mappings up into chunks # (Which are currently defined using MetaSimulations) n_sim = len(simulations) + print(n_sim) chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] for i in range(n_sim % n_processes): chunk_sizes[i] += 1 + print(chunk_sizes) + processes = [] i_start = 0 chunk_nd = [] for chunk in chunk_sizes: + print(f"chunking {chunk}.") if chunk == 0: continue i_end = i_start + chunk @@ -192,9 +196,12 @@ def __init__(self, simulations, mappings, n_processes=None): self.simulations[i_start:i_end], self.mappings[i_start:i_end] ) chunk_nd.append(sim_chunk.survey.nD) + print("creating process") p = _SimulationProcess(sim_chunk) processes.append(p) + print("starting process") p.start() + print("started") i_start = i_end self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index ef79fe11c6..4883fd8656 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -52,7 +52,7 @@ def test_meta_correctness(): serial_sim = MetaSimulation(dc_sims, dc_mappings) print("created serial") - parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings) + parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings, n_processes=2) print("created parallel") try: @@ -145,7 +145,7 @@ def test_sum_correctness(): m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 serial_sim = SumMetaSimulation(g_sims, g_mappings) - parallel_sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings) + parallel_sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings, n_processes=2) try: # test fields objects f_serial = serial_sim.fields(m_test) @@ -219,7 +219,9 @@ def test_repeat_correctness(): repeat_mappings.append(maps.LinearMap(ave_full)) serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) - parallel_sim = MultiprocessingRepeatedSimulation(grav_sim, repeat_mappings) + parallel_sim = MultiprocessingRepeatedSimulation( + grav_sim, repeat_mappings, n_processes=2 + ) t_model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) try: From 5789a964dcf1033a48e48bd59f722882172075f1 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 14:19:28 -0700 Subject: [PATCH 180/455] more debugging --- SimPEG/meta/multiprocessing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 5dd6112288..69ee1c9781 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -46,7 +46,9 @@ def run(self): # everything here is local to the process # this sim is actually local to the running process and will # persist between calls to field, dpred, jvec,... + print("started process") sim = self.sim_chunk + print("got chunk") # a place to cache the field items locally _cached_items = {} @@ -54,7 +56,9 @@ def run(self): # We use them to communicate between the two. t_queue = self.task_queue r_queue = self.result_queue + print("got queues") while True: + print("in run loop") # Get a task from the queue task = t_queue.get() if task is None: From f14a5b005bea457762b8de75369f196f39a151ec Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 14:35:58 -0700 Subject: [PATCH 181/455] test giving a second simulation --- tests/meta/test_multiprocessing_sim.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 4883fd8656..a94c96af9e 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -39,6 +39,7 @@ def test_meta_correctness(): # split by chunks of sources chunk_size = 3 dc_sims = [] + dc_sims2 = [] dc_mappings = [] for i in range(0, len(src_list) + 1, chunk_size): end = min(i + chunk_size, len(src_list)) @@ -48,11 +49,14 @@ def test_meta_correctness(): dc_sims.append( dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) ) + dc_sims2.append( + dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) + ) dc_mappings.append(maps.IdentityMap()) serial_sim = MetaSimulation(dc_sims, dc_mappings) print("created serial") - parallel_sim = MultiprocessingMetaSimulation(dc_sims, dc_mappings, n_processes=2) + parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=2) print("created parallel") try: From b61890257c2446d1d468c256395dc423c9d3abe6 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 15:55:18 -0700 Subject: [PATCH 182/455] explicitly send simulation via the queue --- SimPEG/meta/multiprocessing.py | 62 +++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 69ee1c9781..eb898846c0 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -36,8 +36,8 @@ class _SimulationProcess(Process): returning them to the main processes, unless explicitly asked for... """ - def __init__(self, sim_chunk): - self.sim_chunk = sim_chunk + def __init__(self): # , sim_chunk): + # self.sim_chunk = sim_chunk self.task_queue = Queue() self.result_queue = Queue() super().__init__() @@ -47,7 +47,7 @@ def run(self): # this sim is actually local to the running process and will # persist between calls to field, dpred, jvec,... print("started process") - sim = self.sim_chunk + # sim = self.sim_chunk print("got chunk") # a place to cache the field items locally _cached_items = {} @@ -66,7 +66,12 @@ def run(self): break op, args = task try: - if op == "get_item": + if op == "set_sim": + (sim,) = args + sim_key = uuid.uuid4().hex + _cached_items[sim_key] = sim + r_queue.put(sim_key) + elif op == "get_item": (key,) = args r_queue.put(_cached_items[key]) elif op == "del_item": @@ -74,66 +79,88 @@ def run(self): _cached_items.pop(key, None) elif op == 0: # store_model - (m,) = args + sim_key, m = args + sim = _cached_items[sim_key] sim.model = m elif op == 1: # create fields + (sim_key,) = args + sim = _cached_items[sim_key] f_key = uuid.uuid4().hex r_queue.put(f_key) fields = sim.fields(sim.model) _cached_items[f_key] = fields elif op == 2: # do dpred - (f_key,) = args + sim_key, f_key = args + sim = _cached_items[sim_key] fields = _cached_items[f_key] r_queue.put(sim.dpred(sim.model, fields)) elif op == 3: # do jvec - v, f_key = args + sim_key, v, f_key = args + sim = _cached_items[sim_key] fields = _cached_items[f_key] r_queue.put(sim.Jvec(sim.model, v, fields)) elif op == 4: # do jtvec - v, f_key = args + sim_key, v, f_key = args + sim = _cached_items[sim_key] fields = _cached_items[f_key] r_queue.put(sim.Jtvec(sim.model, v, fields)) elif op == 5: # do jtj_diag - w, f_key = args + sim_key, w, f_key = args + sim = _cached_items[sim_key] fields = _cached_items[f_key] r_queue.put(sim.getJtJdiag(sim.model, w, fields)) except Exception as err: + print(err) r_queue.put(err) + def set_sim(self, sim): + self.task_queue.put(("set_sim", (sim,))) + key = self.result_queue.get() + future = SimpleFuture(key, self.task_queue, self.result_queue) + self._my_sim = future + return future + def store_model(self, m): self._check_closed() - self.task_queue.put((0, (m,))) + sim = self._my_sim + self.task_queue.put((0, (sim.item_id, m))) def get_fields(self): self._check_closed() - self.task_queue.put((1, None)) + sim = self._my_sim + self.task_queue.put((1, (sim.item_id,))) key = self.result_queue.get() future = SimpleFuture(key, self.task_queue, self.result_queue) return future def start_dpred(self, f_future): self._check_closed() - self.task_queue.put((2, (f_future.item_id,))) + sim = self._my_sim + self.task_queue.put((2, (sim.item_id, f_future.item_id))) def start_j_vec(self, v, f_future): self._check_closed() - self.task_queue.put((3, (v, f_future.item_id))) + sim = self._my_sim + self.task_queue.put((3, (sim.item_id, v, f_future.item_id))) def start_jt_vec(self, v, f_future): self._check_closed() - self.task_queue.put((4, (v, f_future.item_id))) + sim = self._my_sim + self.task_queue.put((4, (sim.item_id, v, f_future.item_id))) def start_jtj_diag(self, w, f_future): self._check_closed() + sim = self._my_sim self.task_queue.put( ( 5, ( + sim.item_id, w, f_future.item_id, ), @@ -191,6 +218,7 @@ def __init__(self, simulations, mappings, n_processes=None): processes = [] i_start = 0 chunk_nd = [] + sim_futures = [] for chunk in chunk_sizes: print(f"chunking {chunk}.") if chunk == 0: @@ -201,10 +229,11 @@ def __init__(self, simulations, mappings, n_processes=None): ) chunk_nd.append(sim_chunk.survey.nD) print("creating process") - p = _SimulationProcess(sim_chunk) + p = _SimulationProcess() processes.append(p) print("starting process") p.start() + p.set_sim(sim_chunk) print("started") i_start = i_end @@ -373,9 +402,10 @@ def __init__(self, simulation, mappings, n_processes=None): self.simulation, self.mappings[i_start:i_end] ) chunk_nd.append(sim_chunk.survey.nD) - p = _SimulationProcess(sim_chunk) + p = _SimulationProcess() processes.append(p) p.start() + p.set_sim(sim_chunk) i_start = i_end self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) From 1c67f09a71d1bea1f07defcf876560d803864072 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 16:11:59 -0700 Subject: [PATCH 183/455] change message --- SimPEG/meta/multiprocessing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index eb898846c0..073b1774df 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -233,8 +233,9 @@ def __init__(self, simulations, mappings, n_processes=None): processes.append(p) print("starting process") p.start() - p.set_sim(sim_chunk) print("started") + p.set_sim(sim_chunk) + print("sent sim") i_start = i_end self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) From 7b61ceeaeb1ecc319beeecbf5c4dd94cc7e00e71 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 16:42:26 -0700 Subject: [PATCH 184/455] remove print statements in subprocess --- SimPEG/meta/multiprocessing.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 073b1774df..5a3a17c43d 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -46,9 +46,6 @@ def run(self): # everything here is local to the process # this sim is actually local to the running process and will # persist between calls to field, dpred, jvec,... - print("started process") - # sim = self.sim_chunk - print("got chunk") # a place to cache the field items locally _cached_items = {} @@ -56,9 +53,7 @@ def run(self): # We use them to communicate between the two. t_queue = self.task_queue r_queue = self.result_queue - print("got queues") while True: - print("in run loop") # Get a task from the queue task = t_queue.get() if task is None: @@ -115,10 +110,10 @@ def run(self): fields = _cached_items[f_key] r_queue.put(sim.getJtJdiag(sim.model, w, fields)) except Exception as err: - print(err) r_queue.put(err) def set_sim(self, sim): + self._check_closed() self.task_queue.put(("set_sim", (sim,))) key = self.result_queue.get() future = SimpleFuture(key, self.task_queue, self.result_queue) @@ -213,7 +208,7 @@ def __init__(self, simulations, mappings, n_processes=None): for i in range(n_sim % n_processes): chunk_sizes[i] += 1 - print(chunk_sizes) + print("chunk sizes:") processes = [] i_start = 0 From c4638acdf960729478bfcee6fe904ead8a6b2bd0 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 17:16:49 -0700 Subject: [PATCH 185/455] tests --- SimPEG/meta/multiprocessing.py | 13 ++++++------- tests/meta/test_multiprocessing_sim.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 5a3a17c43d..e977d76f0b 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -36,17 +36,14 @@ class _SimulationProcess(Process): returning them to the main processes, unless explicitly asked for... """ - def __init__(self): # , sim_chunk): - # self.sim_chunk = sim_chunk + def __init__(self): + super().__init__() self.task_queue = Queue() self.result_queue = Queue() - super().__init__() def run(self): # everything here is local to the process - # this sim is actually local to the running process and will - # persist between calls to field, dpred, jvec,... - # a place to cache the field items locally + # a place to cache items locally _cached_items = {} # The queues are shared between the head process and the worker processes @@ -57,6 +54,7 @@ def run(self): # Get a task from the queue task = t_queue.get() if task is None: + t_queue.task_done() # None is a poison pill message to kill this loop. break op, args = task @@ -111,6 +109,7 @@ def run(self): r_queue.put(sim.getJtJdiag(sim.model, w, fields)) except Exception as err: r_queue.put(err) + t_queue.task_done() def set_sim(self, sim): self._check_closed() @@ -208,7 +207,7 @@ def __init__(self, simulations, mappings, n_processes=None): for i in range(n_sim % n_processes): chunk_sizes[i] += 1 - print("chunk sizes:") + print("chunk sizes:", chunk_sizes) processes = [] i_start = 0 diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index a94c96af9e..ec8fb0994a 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -56,7 +56,7 @@ def test_meta_correctness(): serial_sim = MetaSimulation(dc_sims, dc_mappings) print("created serial") - parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=2) + parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=12) print("created parallel") try: From 1157e574290a5938ad2063e4f33ab1c4088afd43 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Jun 2023 17:19:49 -0700 Subject: [PATCH 186/455] remove task done --- SimPEG/meta/multiprocessing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index e977d76f0b..0e82b27c18 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -54,7 +54,6 @@ def run(self): # Get a task from the queue task = t_queue.get() if task is None: - t_queue.task_done() # None is a poison pill message to kill this loop. break op, args = task @@ -109,7 +108,6 @@ def run(self): r_queue.put(sim.getJtJdiag(sim.model, w, fields)) except Exception as err: r_queue.put(err) - t_queue.task_done() def set_sim(self, sim): self._check_closed() From 955a4b460aab5478ae080bfeca28f66659e704c1 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sat, 10 Jun 2023 12:28:18 -0700 Subject: [PATCH 187/455] update the logic for testing if permittivity is None --- .../electromagnetics/frequency_domain/fields.py | 12 ++++++------ .../frequency_domain/simulation.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/SimPEG/electromagnetics/frequency_domain/fields.py index 8c0ca874fa..f0e0a50255 100644 --- a/SimPEG/electromagnetics/frequency_domain/fields.py +++ b/SimPEG/electromagnetics/frequency_domain/fields.py @@ -763,13 +763,13 @@ def _eSecondary(self, bSolution, source_list): s_e = src.s_e(self.simulation) e[:, i] = e[:, i] + -s_e - if getattr(self.simulation, "permittivity", None) is not None: + if self.simulation.permittivity is not None: MeyhatI = self.simulation._get_edge_admittivity_property_matrix( src.frequency, invert_matrix=True ) e[:, i] = MeyhatI * e[:, i] - if getattr(self.simulation, "permittivity", None) is None: + if self.simulation.permittivity is None: return self._MeSigmaI * e else: return e @@ -836,7 +836,7 @@ def _j(self, bSolution, source_list): :return: primary current density """ - if getattr(self.simulation, "permittivity", None) is None: + if self.simulation.permittivity is None: j = self._edgeCurl.T * (self._MfMui * bSolution) for i, src in enumerate(source_list): @@ -1099,13 +1099,13 @@ def _hSecondary(self, jSolution, source_list): :return: secondary magnetic field """ - if getattr(self.simulation, "permittivity", None) is not None: + if self.simulation.permittivity is not None: h = np.zeros((self.mesh.n_edges, len(source_list)), dtype=complex) else: h = self._edgeCurl.T * (self._MfRho * jSolution) for i, src in enumerate(source_list): - if getattr(self.simulation, "permittivity", None) is not None: + if self.simulation.permittivity is not None: h[:, i] = self._edgeCurl.T * ( self.simulation._get_face_admittivity_property_matrix( src.frequency, invert_model=True @@ -1212,7 +1212,7 @@ def _e(self, jSolution, source_list): :rtype: numpy.ndarray :return: electric field """ - # if getattr(self.simulation, "permittivity", None) is None: + # if self.simulation.permittivity is None: return self._MfI * (self._MfRho * self._j(jSolution, source_list)) # e = np.zeros((self.mesh.n_faces, len(source_list)), dtype=complex) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index dd6fb27257..649b02249c 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -69,7 +69,7 @@ def __init__( warnings.warn( "Simulations using permittivity have not yet been thoroughly tested yet, and derivatives are not implemented. Contributions welcome!" ) - self.permittivity = permittivity + self.permittivity = permittivity @property def survey(self): @@ -104,7 +104,7 @@ def forward_only(self, value): self._forward_only = validate_type("forward_only", value, bool) def _get_admittivity(self, freq): - if getattr(self, "permittivity", None) is not None: + if self.permittivity is not None: return self.sigma + 1j * self.permittivity * omega(freq) else: return self.sigma @@ -334,7 +334,7 @@ def getA(self, freq): MfMui = self.MfMui C = self.mesh.edge_curl - if getattr(self, "permittivity", None) is None: + if self.permittivity is None: MeSigma = self.MeSigma A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma else: @@ -501,7 +501,7 @@ def getA(self, freq): C = self.mesh.edge_curl iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - if getattr(self, "permittivity", None) is None: + if self.permittivity is None: MeSigmaI = self.MeSigmaI A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega else: @@ -586,7 +586,7 @@ def getRHS(self, freq): s_m, s_e = self.getSourceTerm(freq) C = self.mesh.edge_curl - if getattr(self, "permittivity", None) is None: + if self.permittivity is None: MeSigmaI = self.MeSigmaI RHS = s_m + C * (MeSigmaI * s_e) else: @@ -702,7 +702,7 @@ def getA(self, freq): C = self.mesh.edge_curl iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - if getattr(self, "permittivity", None) is not None: + if self.permittivity is not None: Mfyhati = self._get_face_admittivity_property_matrix( freq, invert_model=True ) @@ -888,7 +888,7 @@ def getA(self, freq): MeMu = self.MeMu C = self.mesh.edge_curl - if getattr(self, "permittivity", None) is None: + if self.permittivity is None: MfRho = self.MfRho return C.T.tocsr() * (MfRho * C) + 1j * omega(freq) * MeMu else: @@ -953,7 +953,7 @@ def getRHS(self, freq): s_m, s_e = self.getSourceTerm(freq) C = self.mesh.edge_curl - if getattr(self, "permittivity", None) is None: + if self.permittivity is None: MfRho = self.MfRho return s_m + C.T * (MfRho * s_e) else: From c8df6e40feceaa3c414049f9693f9b1d680dc265 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sat, 10 Jun 2023 13:29:52 -0700 Subject: [PATCH 188/455] include a cross-check test that has a contrast in dielectric permittivity --- tests/em/fdem/forward/test_permittivity.py | 202 ++++++++++++++++++++- 1 file changed, 198 insertions(+), 4 deletions(-) diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index 3766a0628a..cd11b152d2 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -49,7 +49,13 @@ def get_inds(val, x, z): def print_comparison( - numeric, analytic, x=np.r_[50, 100], z=np.r_[-100, 100], threshold=threshold + numeric, + analytic, + x=np.r_[50, 100], + z=np.r_[-100, 100], + threshold=threshold, + name1="numeric", + name2="analytic", ): inds = get_inds(numeric, x, z) results = [] @@ -61,9 +67,9 @@ def print_comparison( getattr(analytic[inds], component) - getattr(numeric[inds], component) ) ) - print(f"{component} numeric : {numeric_norm:1.4e}") - print(f"{component} analytic : {analytic_norm:1.4e}") - print(f"{component} difference : {difference:1.4e}\n") + print(f"{component} {name1:12s} : {numeric_norm:1.4e}") + print(f"{component} {name2:12s} : {analytic_norm:1.4e}") + print(f"{component} {'difference':12s} : {difference:1.4e}\n") results.append(difference / np.mean([numeric_norm, analytic_norm]) < threshold) print(results) return results @@ -181,3 +187,191 @@ def test_e_dipole(epsilon, frequency, simulation): print(f"Testing E dipole: {f}") test = print_comparison(fields[:, f].squeeze(), analytic) assert np.all(test) + + +@pytest.mark.parametrize("epsilon_r", epsilon_r_list) +@pytest.mark.parametrize("frequency", frequency_list) +def test_cross_check_e_dipole(epsilon_r, frequency): + tolerance = 1e-8 + + sigma_back = 1e-2 + epsilon_r_back = 1 + + target_z = np.r_[-20, -40] + target_x = np.r_[0, 60] + + frequencies = [frequency] + + sigma = np.ones(mesh.n_cells) * sigma_back + rel_permittivity = np.ones(mesh.n_cells) * epsilon_r_back + + target_inds = ( + (mesh.cell_centers[:, 0] >= target_x.min()) + & (mesh.cell_centers[:, 0] <= target_x.max()) + & (mesh.cell_centers[:, 2] >= target_z.min()) + & (mesh.cell_centers[:, 2] <= target_z.max()) + ) + + rel_permittivity[target_inds] = epsilon_r + + # J-Formulation + sources_j_target = [ + fdem.sources.LineCurrent( + [], freq, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + for freq in frequencies + ] + survey_j_target = fdem.Survey(sources_j_target) + sim_j_target = fdem.Simulation3DCurrentDensity( + mesh, + survey=survey_j_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # H-formulation + sources_h_target = [ + fdem.sources.LineCurrent( + [], freq, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + for freq in frequencies + ] + survey_h_target = fdem.Survey(sources_h_target) + sim_h_target = fdem.Simulation3DMagneticField( + mesh, + survey=survey_h_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # compute fields + fields_j_target = sim_j_target.fields() + fields_h_target = sim_h_target.fields() + + j_comparison = print_comparison( + fields_j_target[:, "j"].squeeze(), + fields_h_target[:, "j"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(j_comparison) + + h_comparison = print_comparison( + fields_j_target[:, "h"].squeeze(), + fields_h_target[:, "h"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(h_comparison) + + e_comparison = print_comparison( + fields_j_target[:, "e"].squeeze(), + fields_h_target[:, "e"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(e_comparison) + + b_comparison = print_comparison( + fields_j_target[:, "b"].squeeze(), + fields_h_target[:, "b"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(h_comparison) + + +@pytest.mark.parametrize("epsilon_r", epsilon_r_list) +@pytest.mark.parametrize("frequency", frequency_list) +def test_cross_check_b_dipole(epsilon_r, frequency): + tolerance = 1e-8 + + sigma_back = 1e-2 + epsilon_r_back = 1 + + target_z = np.r_[-20, -40] + target_x = np.r_[0, 60] + + frequencies = [frequency] + + sigma = np.ones(mesh.n_cells) * sigma_back + rel_permittivity = np.ones(mesh.n_cells) * epsilon_r_back + + target_inds = ( + (mesh.cell_centers[:, 0] >= target_x.min()) + & (mesh.cell_centers[:, 0] <= target_x.max()) + & (mesh.cell_centers[:, 2] >= target_z.min()) + & (mesh.cell_centers[:, 2] <= target_z.max()) + ) + + rel_permittivity[target_inds] = epsilon_r + + # B-Formulation + sources_b_target = [ + fdem.sources.MagDipole([], freq, location=np.r_[0, 0, 0]) + for freq in frequencies + ] + survey_b_target = fdem.Survey(sources_b_target) + sim_b_target = fdem.Simulation3DMagneticFluxDensity( + mesh, + survey=survey_b_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # E-formulation + sources_e_target = [ + fdem.sources.MagDipole([], freq, location=np.r_[0, 0, 0]) + for freq in frequencies + ] + survey_e_target = fdem.Survey(sources_e_target) + sim_e_target = fdem.Simulation3DElectricField( + mesh, + survey=survey_e_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # compute fields + fields_b_target = sim_b_target.fields() + fields_e_target = sim_e_target.fields() + + b_comparison = print_comparison( + fields_b_target[:, "b"].squeeze(), + fields_e_target[:, "b"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(b_comparison) + + e_comparison = print_comparison( + fields_b_target[:, "e"].squeeze(), + fields_e_target[:, "e"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(e_comparison) + + h_comparison = print_comparison( + fields_b_target[:, "h"].squeeze(), + fields_e_target[:, "h"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(h_comparison) + + j_comparison = print_comparison( + fields_b_target[:, "j"].squeeze(), + fields_e_target[:, "j"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(e_comparison) From db976839c9b2d02ee6372ed83ebf018642042359 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sat, 10 Jun 2023 13:39:22 -0700 Subject: [PATCH 189/455] clean out some commented-out code --- .../frequency_domain/simulation.py | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 649b02249c..e72a3285df 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -308,15 +308,6 @@ class Simulation3DElectricField(BaseFDEMSimulation): _formulation = "EB" fieldsPair = Fields3DElectricField - # permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - # # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") - - # def __init__( - # self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs - # ): - # super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) - # self.permittivity = permittivity - def getA(self, freq): r""" System matrix @@ -385,26 +376,6 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) - # def getADeriv_permittivity(self, freq, u, v, adjoint=False): - # r""" - # Product of the derivative of our system matrix with respect to the - # permittivity model and a vector - - # :param float freq: frequency - # :param numpy.ndarray u: solution vector (nE,) - # :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - # adjoint - # :param bool adjoint: adjoint? - # :rtype: numpy.ndarray - # :return: derivative of the system matrix times a vector (nP,) or - # adjoint (nD,) - # """ - - # if getattr(self, "permittivityMap", None) is not None: - # return Zero() - # dMe_dpermittivity_v = self.MePermittivityDeriv(u, v, adjoint) - # return -omega(freq) ** 2 * dMe_dpermittivity_v - def getADeriv(self, freq, u, v, adjoint=False): return ( self.getADeriv_sigma(freq, u, v, adjoint) From 360ca3089273d65ba1e1c9d14c072ad45619f0d2 Mon Sep 17 00:00:00 2001 From: dccowan Date: Sat, 10 Jun 2023 14:59:30 -0700 Subject: [PATCH 190/455] Full draft of regularization docs --- SimPEG/regularization/__init__.py | 11 +- SimPEG/regularization/correspondence.py | 18 +- SimPEG/regularization/pgi.py | 8 +- SimPEG/regularization/sparse.py | 22 +- SimPEG/regularization/vector.py | 722 +++++++++++++++++++++++- 5 files changed, 730 insertions(+), 51 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 0f6f911095..5fd1e2dde9 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -106,12 +106,16 @@ Vector Regularizations ---------------------- -Regularization functions for models defining vector properties. +Vector regularization allows for the recovery of vector models; that is, a model +where the parameters for each cell define directional components of a vector quantity. .. autosummary:: :toctree: generated/ CrossReferenceRegularization + VectorAmplitude + AmplitudeSmallness + AmplitudeSmoothnessFirstOrder Joint Regularizations --------------------- @@ -139,6 +143,7 @@ BaseSimilarityMeasure BaseSparse BaseVectorRegularization + BaseAmplitude """ from ..utils.code_utils import deprecate_class @@ -160,10 +165,10 @@ BaseVectorRegularization, CrossReferenceRegularization, VectorAmplitude, + AmplitudeSmallness, + AmplitudeSmoothnessFirstOrder, ) -from .vector import BaseVectorRegularization, CrossReferenceRegularization - @deprecate_class(removal_version="0.19.0", future_warn=True) class SimpleSmall(Smallness): diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index ff2c8d9474..f5fd7fa975 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -12,7 +12,7 @@ class LinearCorrespondence(BaseSimilarityMeasure): ``LinearCorrespondence`` is used to recover a model where the differences between the model parameter values for two physical property types are minimal. ``LinearCorrespondence`` can also be used to minimize the squared L2-norm of a linear combination of model parameters - for two physical property types. + for two physical property types. See the *Notes* section for a comprehensive description. Parameters ---------- @@ -59,11 +59,23 @@ def __init__(self, mesh, wire_map, coefficients=None, **kwargs): @property def coefficients(self): - """Coefficients for the linear relationship between model parameters. + r"""Coefficients for the linear relationship between model parameters. + + For a relation vector: + + .. math:: + \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 + + where + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + This property defines the coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}`. Returns ------- - (3) numpy.ndarray of float + (3, ) numpy.ndarray of float Coefficients for the linear relationship between model parameters. """ return self._coefficients diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index c3c40d50bf..944f8555cb 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -1054,7 +1054,7 @@ class PGI(ComboObjectiveFunction): - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is the weighting matrix for PGI smallness, and - - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices for smoothness terms. + - :math:`\mathbf{W_x, \, W_y, \; W_z}` are weighting matrices for smoothness terms. ``PGIsmallness`` regularization can be used for models consisting of one or more physical property types. The ordering of the physical property types within the model is defined @@ -1070,8 +1070,8 @@ class PGI(ComboObjectiveFunction): regularization function (objective function) can be expressed as: .. math:: - \phi (\mathbf{m}) &= \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, - \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 + \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) @@ -1290,7 +1290,7 @@ def compute_quasi_geology_model(self): `compute_quasi_geology_model` method computes: .. math:: - g_i^ = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 + g_i = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 where :math:`\mathbf{m}_i` are the model parameter values for cell :math:`i` for the current model. The ordering of the output vector :math:`\mathbf{g}` is the same as the diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 6211672ecb..42d081fef9 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -256,7 +256,7 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \Big | m(r) - m^{(ref)}(r) \Big |^{p(r)} \, dv + \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes @@ -462,7 +462,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, - \bigg | \frac{\partial m}{\partial x} \bigg |^{p(r)} \, dv + \Bigg | \, \frac{\partial m}{\partial x} \, \Bigg |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`w(r)` is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes @@ -507,13 +507,13 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, - \mathbf{W \, R}^{\!\! (k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 where - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), - - :math:`\mathbf{W}^{(k)` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix acts on variables living on x-faces. @@ -522,17 +522,11 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be preserved by including the reference model the smoothness regularization. - In this case, the objective function becomes: - - .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, R \, G_x} - \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - The least-squares problem for IRLS iteration :math:`k` becomes: + In this case, the least-squares problem for IRLS iteration :math:`k` becomes: .. math:: \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, - \mathbf{W}^{\! (k)} \mathbf{G_x} + \mathbf{W}^{(k)} \mathbf{G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the @@ -763,9 +757,9 @@ class Sparse(WeightedLeastSquares): .. math:: \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) - \Big [ m(r) - m^{(ref)}(r) \Big ]^{p_s(r)} \, dv + \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) - \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^{p_j(r)} \, dv + \Bigg | \, \frac{\partial m}{\partial \xi_j} \, \Bigg |^{p_j(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function applied to all terms. diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 0146bda72c..e6ad4640eb 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -371,12 +371,29 @@ def W(self): class BaseAmplitude(BaseVectorRegularization): - """ - Base vector amplitude class. - Requires a mesh and a :obj:`SimPEG.maps.Wires` mapping. + """Base amplitude regularization class for models defined by vector quantities. + + The ``BaseAmplitude`` class defines properties and methods used + by amplitude regularization classes for vector quantities. + It is not directly used to constrain inversions. """ def amplitude(self, m): + """Return vector amplitudes for the model provided. + + Where the model `m` defines a vector quantity for each active cell in the + inversion, the `amplitude` method returns the amplitudes of these vectors. + + Parameters + ---------- + m : (nP, ) numpy.ndarray + The model. + + Returns + ------- + (n_cells, ) numpy.ndarray + The amplitudes of the vectors for the model provided. + """ return np.linalg.norm( (self.mapping * self._delta_m(m)).reshape( (self.regularization_mesh.nC, self.n_comp), order="F" @@ -385,7 +402,25 @@ def amplitude(self, m): ) def deriv(self, m) -> np.ndarray: - """ """ + r"""Jacobian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. + I.e.: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Jacobian is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The Jacobian of the regularization function evaluated for the model provided. + """ d_m = self._delta_m(m) return self.f_m_deriv(m).T * ( @@ -395,7 +430,33 @@ def deriv(self, m) -> np.ndarray: ).flatten(order="F") def deriv2(self, m, v=None) -> csr_matrix: - """ """ + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the regularization + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ f_m_deriv = self.f_m_deriv(m) if v is None: @@ -409,19 +470,189 @@ def deriv2(self, m, v=None) -> csr_matrix: class AmplitudeSmallness(SparseSmallness, BaseAmplitude): - """ - Sparse smallness regularization on vector amplitude. + r"""Sparse smallness regularization on vector amplitudes. + + ``AmplitudeSmallness`` is a sparse norm smallness regularization that acts on the + amplitudes of the vectors defining the model. Sparse norm functionality allows the + use to recover more compact regions of higher amplitude vectors. + The level of compactness is controlled by the norm within the regularization + function; with more compact structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of compactness + being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + norm : float, (n_cells, ) array_like + The norm defining sparseness in the regularization function. Use a ``float`` to define + the same norm for all mesh cells, or define an independent norm for each cell. All norm + values must be within the interval [0, 2]. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + We define the regularization function (objective function) for sparse amplitude smallness + (compactness) as: + + .. math:: + \phi (\vec{m}) = \frac{1}{2} \int_\Omega \, w(r) \, + \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. More compact structures are recovered in regions + where :math:`p` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discretized approximation for the regularization + function (objective function) is expressed in linear form as: + + .. math:: + \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \Big | \vec{m}_i - \vec{m}_i^{(ref)} \Big |^{p_i} + + where :math:`\mathbf{m}` are the model parameters, :math:`\vec{m}_i` represents the vector + defined for mesh cell :math:`i`, and :math:`\vec{m}_i^{(ref)}` defines the reference model + vector for cell :math:`i`. :math:`\tilde{w}_i` are amalgamated weighting constants that + 1) account for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^{p_i} + \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \, \Big | \vec{m}_i^{(k-1)} - \vec{m}_i^{(ref)} \Big |^2 + + \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + We define the amplitudes of the residual between the model and reference model for all cells as: + + .. math:: + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + The objective function for IRLS iteration :math:`k` is given by: + + .. math:: + \phi \big ( \mathbf{\bar{m}}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{\bar{m}}^{(k)} \; \Big \|^2 + + where + + - :math:`\mathbf{\bar{m}}^{(k)}` are the absolute values of the residual at iteration :math:`k`, and + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights + for iteration :math:`k`. The net weighting applied within the objective function + is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + For a description of how IRLS weights are updated at every iteration, see the documentation + for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = AmplitudeSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) """ def f_m(self, m): - """ - Compute the amplitude of a vector model. + r"""Evaluate the regularization kernel function. + + For smallness vector amplitude regularization, the regularization kernel function is: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + where the global set of model parameters :math:`\mathbf{m}` defined at cell centers is + ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) + directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + Likewise for the vector components of the reference model. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated at the model provided. + """ return self.amplitude(m) @property def W(self): + ### Inherited from Smallness regularization class if getattr(self, "_W", None) is None: mesh = self.regularization_mesh nC = mesh.nC @@ -443,8 +674,187 @@ def W(self): class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): - """ - Sparse first spatial derivatives of amplitude. + r"""Sparse amplitude smoothness (blockiness) regularization. + + ``AmplitudeSmallness`` is a sparse norm smoothness regularization that acts on the + amplitudes of the vectors defining the model. Sparse norm functionality allows the + use to recover more blocky regions of higher amplitude vectors. + The level of blockiness is controlled by the norm within the regularization + function; with more blocky structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of blockiness + being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + orientation : {'x','y','z'} + The direction along which sparse smoothness is applied. + norm : float, array_like + The norm defining sparseness thoughout the regularization function. Must be within the + interval [0,2]. There are several options: + + - ``float``: constant sparse norm throughout the domain. + - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). + - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). + + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total + gradient or components of the gradient. + + Notes + ----- + The regularization function (objective function) for sparse amplitude smoothness (blockiness) + along the x-direction as: + + .. math:: + \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \Bigg | \, \frac{\partial |\vec{m}|}{\partial x} \, \Bigg |^{p(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. Sharper boundaries are recovered in regions + where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discrete approximation for the regularization + function (objective function) is expressed in linear form as: + + .. math:: + \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \tilde{w}_i \, \Bigg | \, \frac{\partial |\vec{m}_i|}{\partial x} \, \Bigg |^{p_i} + + where :math:`\vec{m}_i` is the vector defined for mesh cell :math:`i`. + :math:`\tilde{w}_i` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \frac{1}{2} \sum_i + \tilde{w}_i \, \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^{p_i} + \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial \big | \vec{m}_i^{(k-1)} \big | }{\partial x} \Bigg )^2 + + \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + We define the amplitudes of the vectors for all cells as: + + .. math:: + \mathbf{\bar{m}} = \Big [ \, \mathbf{m}_p^2 + \mathbf{m}_s^2 + \mathbf{m}_t^2 \Big ]^{1/2} + + The objective function for IRLS iteration :math:`k` is given by: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 + + where + + - :math:`\bar{\mathbf{m}}^{(k)}` are the discrete vector model amplitudes at iteration :math:`k`, + - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix + acts on variables living on x-faces. + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization. + In this case, the least-squares problem for IRLS iteration :math:`k` becomes: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 + + where + + .. math:: + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom weights defined on the faces specified by the `orientation` property; + i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined + directly on the faces or have been averaged from cell centers. And let + :math:`\mathbf{r_x}^{\!\! (k)}` represent the IRLS weights for iteration :math:`k`. + The net weighting applied within the objective function is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{v_x} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces; i.e. where the + x-derivative lives. For a description of how IRLS weights are updated at every iteration, + see the documentation for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: """ @property @@ -470,22 +880,77 @@ def _weights_shapes(self) -> list[tuple[int]]: ] def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For first-order smoothness regularization in the x-direction, + the regularization kernel function is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x \, \bar{m}} + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction + (i.e. x-derivative), and + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is + ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) + directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + Likewise for the reference model vector. The expression has the same form for smoothness + along y and z. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated for the model provided. + """ a = self.amplitude(m) return self.cell_gradient @ a def f_m_deriv(self, m) -> csr_matrix: - """""" + r"""Derivative of the regularization kernel function. + + For first-order smoothness regularization in the x-direction, the derivative of the + regularization kernel function with respect to the model is given by: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \begin{bmatrix} \mathbf{G_x} & \mathbf{0} \\ \mathbf{0} & \mathbf{G_x} \end{bmatrix} + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along x + (i.e. the x-derivative). + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + """ return sp.block_diag([self.cell_gradient] * self.n_comp) @ self.mapping.deriv( self._delta_m(m) ) @property def W(self): - """ - Weighting matrix that takes the volumes, free weights, fixed weights and - length scales of the difference operator (normalized optional). - """ + ### Inherited if getattr(self, "_W", None) is None: average_cell_2_face = getattr( self.regularization_mesh, "aveCC2F{}".format(self.orientation) @@ -511,26 +976,229 @@ def W(self): class VectorAmplitude(Sparse): - r""" - The regularization is: + r"""Sparse vector amplitude regularization. + + Apply vector amplitude regularization for recovering compact and/or blocky structures + using a weighted sum of :class:`AmplitudeSmallness` and :class:`AmplitudeSmoothnessFirstOrder` + regularization functions. The level of compactness and blockiness is + controlled by the norms within the respective regularization functions; + with more sparse structures (compact and/or blocky) being recovered when smaller + norms are used. Optionally, custom cell weights can be applied to control + the degree of sparseness being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. - The function defined here approximates: + Parameters + ---------- + mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + mapping : None, SimPEG.maps.BaseMap + The mapping from the model parameters to the active cells in the inversion. + If ``None``, the mapping is the identity map. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + alpha_s : float, optional + Scaling constant for the smallness regularization term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the + total gradient or components of the gradient. + norms : (dim+1, ) numpy.ndarray + The respective norms used for the sparse smallness, x-smoothness, (y-smoothness + and z-smoothness) regularization function. Must all be within the interval [0, 2]. + E.g. `np.r_[2, 1, 1, 1]` uses a 2-norm on the smallness term and a 1-norm on all + smoothness terms. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization + function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + Sparse vector amplitude regularization can be defined by a weighted sum of + :class:`AmplitudeSmallness` and :class:`AmplitudeSmoothnessFirstOrder` + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: + + .. math:: + \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) + \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + \Bigg | \, \frac{\partial |\vec{m}|}{\partial \xi_j} \, \bigg |^{p_j(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, + and :math:`w(r)` is a user-defined weighting function applied to all terms. + :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. + Parameters :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` are multiplier constants + that weight the respective contributions of the smallness and smoothness terms in the + regularization. :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness + throughout the recovered model; where more compact structures are recovered in regions where + :math:`p_s(r)` is small. And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which + impose sparse smoothness throughout the recovered model along the specified direction; + where sharper boundaries are recovered in regions where these parameters are small. + + For implementation within SimPEG, regularization functions and their variables + must be discretized onto a `mesh`. For a regularization function whose kernel is given by + :math:`f(r)`, we approximate as follows: .. math:: + \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} - \phi_m(\mathbf{m}) = \alpha_s \| \mathbf{W}_s \; \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_x \| \mathbf{W}_x \; \frac{\partial}{\partial x} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_y \| \mathbf{W}_y \; \frac{\partial}{\partial y} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p - + \alpha_z \| \mathbf{W}_z \; \frac{\partial}{\partial z} \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) \|_p + where :math:`f_i \in \mathbf{f_m}` define the discrete regularization kernel function + on the mesh such that: - where $\mathbf{a}(\mathbf{m} - \mathbf{m_{ref})$ is the vector amplitude of the difference between - the model and the reference model. + .. math:: + f_i = \begin{cases} + | \, \vec{m}_i \, | \;\;\;\;\;\;\; (no \; reference \; model)\\ + | \, \vec{m}_i - \vec{m}_i^{(ref)} \, | \;\;\;\; (reference \; model) + \end{cases} + + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). + + It is impractical to work with sparse norms directly, as their derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate sparse norms by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \sum_i \tilde{w}_i \, \Big | f_i^{(k)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | f_i^{(k)} \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + The objective function for IRLS iteration :math:`k` can be expressed as a weighted sum of + objective functions of the form: + + .. math:: + \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \bar{m}} \, \Big \|^2 + + where .. math:: + \Delta \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + and + + .. math:: + \mathbf{\bar{m}} = \Big [ \, \mathbf{m}_p^2 + \mathbf{m}_s^2 + \mathbf{m}_t^2 \Big ]^{1/2} + + :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, and + :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are the weighting matrices for iteration :math:`k`. + The weighting matrices apply the IRLS weights, user-defined weighting, and account for cell + dimensions when the regularization functions are discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell + weights that are applied to all objective functions in the model objective function. + For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness + term is given by: + + .. math:: + \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot + \mathbf{v} \odot \prod_j \mathbf{w_j} + + And for sparse smoothness along x (likewise for y and z) is given by: + + .. math:: + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \big ( \mathbf{P_x \, v} \big ) + \odot \prod_j \mathbf{P_x \, w_j} + + The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` + for :math:`\ast = s,x,y,z`. :math:`\mathbf{v}` are the cell volumes. + Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` + project to the appropriate faces. + + Once the net weights for all objective functions are computed, + their weighting matrices can be constructed via: + + .. math:: + \mathbf{W}_\ast^{(k)} = \textrm{diag} \Big ( \, \sqrt{\mathbf{w_\ast}^{\!\! (k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = AmplitudeVector(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model can be preserved by including the + reference model the smoothness regularization. In this case, + the objective function becomes: + + .. math:: + \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \Delta \bar{m}} \, \Big \|^2 + + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Alphas and length scales:** + + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` parameters are set to 0 by default. + + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: + + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and - \mathbf{a}(\mathbf{m} - \mathbf{m_{ref}) = [\sum_{i}^{N}(\mathbf{P}_i\;(\mathbf{m} - \mathbf{m_{ref}}))^{2}]^{1/2} + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 - where :math:`\mathbf{P}_i` is the projection of i-th component of the vector model with N-dimensions. + Likewise for y and z. """ From 7b5296424fb194f74dfcc352e72389ac6912da2b Mon Sep 17 00:00:00 2001 From: dccowan Date: Sat, 10 Jun 2023 16:07:27 -0700 Subject: [PATCH 191/455] black and flake8 --- SimPEG/regularization/__init__.py | 4 +- SimPEG/regularization/base.py | 40 +++++++-------- SimPEG/regularization/correspondence.py | 26 +++++----- SimPEG/regularization/cross_gradient.py | 21 ++++---- SimPEG/regularization/jtv.py | 16 +++--- SimPEG/regularization/pgi.py | 56 ++++++++++---------- SimPEG/regularization/sparse.py | 40 +++++++-------- SimPEG/regularization/vector.py | 68 ++++++++++++------------- 8 files changed, 137 insertions(+), 134 deletions(-) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 5fd1e2dde9..25625de08b 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -32,7 +32,7 @@ The global objective function contains two terms: a data misfit term :math:`\phi_d` which ensures data predicted by the recovered model adequately reproduces the observed data, -and the model objective function :math:`\phi_m` which is comprised of one or more +and the model objective function :math:`\phi_m` which is comprised of one or more regularization functions (objective functions) :math:`\phi_i (m)`. I.e.: .. math:: @@ -51,7 +51,7 @@ y-directions can be expressed as: .. math:: - \phi_m (m) = + \phi_m (m) = \alpha_s \! \int_\Omega \Bigg [ \frac{1}{2} w_s(r) \, m(r)^2 \Bigg ] \, dv + \alpha_x \! \int_\Omega \Bigg [ \frac{1}{2} w_x(r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index cee042c3ca..94e0ad42db 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -448,11 +448,11 @@ def f_m_deriv(self, m) -> csr_matrix: @utils.timeIt def deriv(self, m) -> np.ndarray: - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - I.e.: + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} @@ -460,12 +460,12 @@ def deriv(self, m) -> np.ndarray: Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the Jacobian is evaluated. + The model for which the gradient is evaluated. Returns ------- (n_param, ) numpy.ndarray - The Jacobian of the regularization function evaluated for the model provided. + The Gradient of the regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) return self.f_m_deriv(m).T * (self.W.T * r) @@ -518,7 +518,7 @@ class Smallness(BaseRegularization): regions the model. See the *Notes* section below for a comprehensive description. - + Parameters ---------- mesh : .regularization.RegularizationMesh @@ -760,7 +760,7 @@ class SmoothnessFirstOrder(BaseRegularization): \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization function (objective function) is expressed in linear form as: @@ -786,11 +786,11 @@ class SmoothnessFirstOrder(BaseRegularization): :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. **Reference model in smoothness:** - + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be preserved by including the reference model the regularization. In this case, the objective function becomes: - + .. math:: \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 @@ -798,7 +798,7 @@ class SmoothnessFirstOrder(BaseRegularization): This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. - + **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of @@ -988,7 +988,7 @@ def f_m_deriv(self, m) -> csr_matrix: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} where :math:`\mathbf{G_x}` is the partial cell gradient operator along x - (i.e. the x-derivative). + (i.e. the x-derivative). Parameters ---------- @@ -1125,7 +1125,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization function (objective function) is expressed in linear form as: @@ -1148,11 +1148,11 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): - :math:`\mathbf{W}` is the weighting matrix. **Reference model in smoothness:** - + Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be preserved by including the reference model the smoothness regularization function. In this case, the objective function becomes: - + .. math:: \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 @@ -1160,7 +1160,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. - + **Custom weights and the weighting matrix:** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of @@ -1221,7 +1221,7 @@ def f_m(self, m): \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second-order x-derivative operator, and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. @@ -1256,7 +1256,7 @@ def f_m_deriv(self, m) -> csr_matrix: .. math:: \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - + where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. Parameters @@ -1279,7 +1279,7 @@ def f_m_deriv(self, m) -> csr_matrix: \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the + :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the second-order x-derivative operator, and :math:`\mathbf{W}` is the weighting matrix. Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. @@ -1438,11 +1438,11 @@ class WeightedLeastSquares(ComboObjectiveFunction): - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices. **Reference model in smoothness:** - + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be preserved by including the reference model the smoothness regularization. In this case, the objective function becomes: - + .. math:: \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index f5fd7fa975..6f31dd3317 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -29,7 +29,7 @@ class LinearCorrespondence(BaseSimilarityMeasure): Coefficients :math:`\{ \lambda_1, \lambda_2, \lambda_3 \}` for the linear relationship between model parameters. If ``None``, the coefficients are set to :math:`\{ 1, -1, 0 \}`. - + Notes ----- Let :math:`\mathbf{m}` be a discrete model consisting of two physical property types such that: @@ -98,7 +98,7 @@ def relation(self, model): :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` as follows: .. math:: - \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 + \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 Parameters ---------- @@ -133,16 +133,17 @@ def __call__(self, model): return 0.5 * result.T @ result def deriv(self, model): - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - For a model :math:`\mathbf{m}` consisting of two physical properties such that: + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - The Jacobian has the form: + + The gradient has the form: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} = @@ -157,7 +158,7 @@ def deriv(self, model): Returns ------- (n_param, ) numpy.ndarray - Jacobian of the regularization function evaluated for the model provided. + Gradient of the regularization function evaluated for the model provided. """ k1, k2, k3 = self.coefficients r = self.relation(model) @@ -172,12 +173,13 @@ def deriv2(self, model, v=None): r"""Hessian of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: - For a model :math:`\mathbf{m}` consisting of two physical properties such that: + this method evalutate and returns the second derivative (Hessian) with respect to the + model parameters. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - + The Hessian has the form: .. math:: @@ -194,7 +196,7 @@ def deriv2(self, model, v=None): .. math:: \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} - + Parameters ---------- model : (n_param, ) numpy.ndarray diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index 56fbf5e583..9c8193a19f 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -61,7 +61,7 @@ class CrossGradient(BaseSimilarityMeasure): .. math:: \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, - \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 + \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv For implementation within SimPEG, the regularization function and its variables @@ -70,7 +70,7 @@ class CrossGradient(BaseSimilarityMeasure): .. math:: \phi (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ - \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 + \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and @@ -254,16 +254,17 @@ def __call__(self, model): ) def deriv(self, model): - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - For a model :math:`\mathbf{m}` consisting of two physical properties such that: + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - The Jacobian has the form: + + The gradient has the form: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} = @@ -278,7 +279,7 @@ def deriv(self, model): Returns ------- (n_param, ) numpy.ndarray - Jacobian of the regularization function evaluated for the model provided. + Gradient of the regularization function evaluated for the model provided. """ m1, m2 = self.wire_map * model @@ -303,7 +304,7 @@ def deriv2(self, model, v=None): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - + The Hessian has the form: .. math:: @@ -320,7 +321,7 @@ def deriv2(self, model, v=None): .. math:: \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} - + Parameters ---------- model : (n_param, ) numpy.ndarray diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index b1926b0759..aa55780b47 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -89,7 +89,7 @@ class JointTotalVariation(BaseSimilarityMeasure): - :math:`\mathbf{A}` averages vectors from faces to cell centers, - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), - :math:`\mathbf{v}` are the cell volumes, and - - :math:`\epsilon` is a constant added for continuous differentiability (set with the `eps` property), + - :math:`\epsilon` is a constant added for continuous differentiability (set with the `eps` property), **Custom weights and the weighting matrix:** @@ -196,17 +196,17 @@ def __call__(self, model): return np.sum(sq) def deriv(self, model): - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - For a model :math:`\mathbf{m}` consisting of multiple physical properties + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of multiple physical properties :math:`\mathbf{m_1}, \; \mathbf{m_2}, \; ...` such that: .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} - - The Jacobian has the form: + + The gradient has the form: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} = @@ -221,7 +221,7 @@ def deriv(self, model): Returns ------- (n_param, ) numpy.ndarray - Jacobian of the regularization function evaluated for the model provided. + Gradient of the regularization function evaluated for the model provided. """ W = self.W G = self._G @@ -250,7 +250,7 @@ def deriv2(self, model, v=None): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} - + The Hessian has the form: .. math:: diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 944f8555cb..e1340cf79c 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -115,7 +115,7 @@ class PGIsmallness(Smallness): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} - + When the `approx_eval` property is ``True``, we assume the physical property types have values that are uncorrelated. In this case, the weighting matrix is diagonal and the regularization function (objective function) can be expressed as: @@ -123,7 +123,7 @@ class PGIsmallness(Smallness): .. math:: \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 - + When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property to ``True`` so that the least-squares approximation is used to compute the gradient. @@ -135,22 +135,22 @@ class PGIsmallness(Smallness): .. math:: \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} - + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: .. math:: \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, - diag \big ( \mathbf{v \odot w} \big ) - + diag \big ( \mathbf{v \odot w} \big ) + where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal covariances are zero and we can use a weighting matrix of the form: .. math:: \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} - \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) - + \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) + where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the membership array :math:`\mathbf{z}^\ast`. @@ -165,7 +165,7 @@ class PGIsmallness(Smallness): .. math:: \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) - + using a MAP variation of the expectation-maximization clustering algorithm introduced in Dempster (et al. 1977). @@ -179,7 +179,7 @@ class PGIsmallness(Smallness): .. math:: z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) - + where - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, @@ -297,11 +297,11 @@ def membership(self, m): For a Gaussian mixture model containing the means and covariances for model parameter types (physical property types) for all rock units, this method computes the membership array for the model `m` provided. For a description of the membership array, see the - *Notes* section within the :class:`PGIsmallness` documentation. + *Notes* section within the :class:`PGIsmallness` documentation. Parameters ---------- - m : (nP, ) numpy.ndarray of float + m : (n_param, ) numpy.ndarray of float The model. Returns @@ -322,7 +322,7 @@ def compute_quasi_geology_model(self): Returns ------- - (nP, ) numpy.ndarray + (n_param, ) numpy.ndarray The quasi geology physical property model. Notes @@ -523,11 +523,11 @@ def __call__(self, m, external_weights=True): @timeIt def deriv(self, m): - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - I.e.: + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} @@ -535,12 +535,12 @@ def deriv(self, m): Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the Jacobian is evaluated. + The model for which the gradient is evaluated. Returns ------- (n_param, ) numpy.ndarray - The Jacobian of the regularization function evaluated for the model provided. + Gradient of the regularization function evaluated for the model provided. """ if getattr(self, "reference_model", None) is None: self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) @@ -1064,7 +1064,7 @@ class PGI(ComboObjectiveFunction): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} - + When the `approx_eval` property is ``True``, we assume the physical property types have values that are uncorrelated. In this case, the weighting matrix is diagonal and the regularization function (objective function) can be expressed as: @@ -1075,7 +1075,7 @@ class PGI(ComboObjectiveFunction): &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - + When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property to ``True`` so that the least-squares approximation is used to compute the gradient. @@ -1087,22 +1087,22 @@ class PGI(ComboObjectiveFunction): .. math:: \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} - + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: .. math:: \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, - diag \big ( \mathbf{v \odot w} \big ) - + diag \big ( \mathbf{v \odot w} \big ) + where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal covariances are zero and we can use a weighting matrix of the form: .. math:: \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} - \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) - + \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) + where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the membership array :math:`\mathbf{z}^\ast`. @@ -1117,7 +1117,7 @@ class PGI(ComboObjectiveFunction): .. math:: \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) - + using a MAP variation of the expectation-maximization clustering algorithm introduced in Dempster (et al. 1977). @@ -1131,7 +1131,7 @@ class PGI(ComboObjectiveFunction): .. math:: z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) - + where - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, @@ -1259,7 +1259,7 @@ def membership(self, m): Parameters ---------- - m : (nP, ) numpy.ndarray of float + m : (n_param ) numpy.ndarray of float The model. Returns @@ -1278,7 +1278,7 @@ def compute_quasi_geology_model(self): Returns ------- - (nP, ) numpy.ndarray + (n_param ) numpy.ndarray The quasi geology physical property model. Notes diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 42d081fef9..d81d19be1c 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -55,7 +55,7 @@ class BaseSparse(BaseRegularization): If ``False``, do not scale. irls_threshold : float Constant added to IRLS weights to ensures stability in the algorithm. - + """ def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): @@ -166,17 +166,17 @@ def get_lp_weights(self, f_m): and :math:`\mathbf{p}` defines the `norm` at each cell. :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). + (when the `irls_scaled` property is ``True``). The scaling acts to preserve the balance between the data misfit and objective functions in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. - + To apply elementwise scaling, let .. math:: f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - And define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + And define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: .. math:: \tilde{f}_{\! i,max} = \begin{cases} @@ -263,7 +263,7 @@ class SparseSmallness(BaseSparse, Smallness): sparseness throughout the recovered model. More compact structures are recovered in regions where :math:`p` is small. If the same level of sparseness is being imposed everywhere, the exponent becomes a constant. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization function (objective function) is expressed in linear form as: @@ -370,17 +370,17 @@ def update_weights(self, m): and :math:`\mathbf{p}` defines the norm for each cell (defined using the `norm` property). :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). + (when the `irls_scaled` property is ``True``). The scaling acts to preserve the balance between the data misfit and objective functions in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. - + To compute the scaling, let .. math:: f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: .. math:: \tilde{f}_{\! i,max} = \begin{cases} @@ -406,7 +406,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): function; with more blocky structures being recovered when a smaller norm is used. Optionally, custom cell weights can be included to control the degree of blockiness being enforced throughout different regions the model. - + See the *Notes* section below for a comprehensive description. Parameters @@ -469,7 +469,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): sparseness throughout the recovered model. Sharper boundaries are recovered in regions where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, the exponent becomes a constant. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discrete approximation for the regularization function (objective function) is expressed in linear form as: @@ -625,17 +625,17 @@ def update_weights(self, m): and :math:`\mathbf{p}` defines the norm for each element (set using the `norm` property). :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). + (when the `irls_scaled` property is ``True``). The scaling acts to preserve the balance between the data misfit and objective functions in the regularization, and improves convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. - + To apply the scaling, let .. math:: f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: .. math:: \tilde{f}_{\! i,max} = \begin{cases} @@ -675,7 +675,7 @@ def gradient_type(self) -> str: Returns ------- str in {"total", "components"} - Whether to re-weight using the total gradient or partial gradients along + Whether to re-weight using the total gradient or partial gradients along smoothing orientations. """ return self._gradient_type @@ -760,7 +760,7 @@ class Sparse(WeightedLeastSquares): \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) \Bigg | \, \frac{\partial m}{\partial \xi_j} \, \Bigg |^{p_j(r)} \, dv - + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function applied to all terms. :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. @@ -785,7 +785,7 @@ class Sparse(WeightedLeastSquares): .. math:: \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)} - + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account for cell dimensions in the discretization and 2) apply user-defined weighting. :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). @@ -863,11 +863,11 @@ class Sparse(WeightedLeastSquares): >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) **Reference model in smoothness:** - + Gradients/interfaces within a discrete reference model can be preserved by including the reference model the smoothness regularization. In this case, the objective function becomes: - + .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} \Big \| \mathbf{W_s}^{\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 @@ -972,7 +972,7 @@ def gradient_type(self) -> str: Returns ------- str in {"total", "components"} - Whether to re-weight using the total gradient or partial gradients along + Whether to re-weight using the total gradient or partial gradients along smoothing orientations. """ return self._gradient_type @@ -994,7 +994,7 @@ def norms(self): """Norms for the child regularization classes. Norms for the smallness and all smoothness terms in the ``Sparse`` regularization. - + Returns ------- list of float or numpy.ndarray diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index e6ad4640eb..96caf7591a 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -52,7 +52,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): in the model and reference vector model are parallel (or anti-parallel) to each other. And it is maximized when the vectors are perpendicular to each other. The reference vector model can be set using a single vector, or by defining a - vector for each mesh cell. + vector for each mesh cell. Parameters ---------- @@ -386,7 +386,7 @@ def amplitude(self, m): Parameters ---------- - m : (nP, ) numpy.ndarray + m : (n_param ) numpy.ndarray The model. Returns @@ -402,11 +402,11 @@ def amplitude(self, m): ) def deriv(self, m) -> np.ndarray: - r"""Jacobian of the regularization function evaluated for the model provided. + r"""Gradient of the regularization function evaluated for the model provided. Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative (Jacobian) with respect to the model parameters. - I.e.: + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient: .. math:: \frac{\partial \phi}{\partial \mathbf{m}} @@ -414,12 +414,12 @@ def deriv(self, m) -> np.ndarray: Parameters ---------- m : (n_param, ) numpy.ndarray - The model for which the Jacobian is evaluated. + The model for which the gradient is evaluated. Returns ------- (n_param, ) numpy.ndarray - The Jacobian of the regularization function evaluated for the model provided. + Gradient of the regularization function evaluated for the model provided. """ d_m = self._delta_m(m) @@ -527,7 +527,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): sparseness throughout the recovered model. More compact structures are recovered in regions where :math:`p` is small. If the same level of sparseness is being imposed everywhere, the exponent becomes a constant. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discretized approximation for the regularization function (objective function) is expressed in linear form as: @@ -560,7 +560,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: @@ -570,9 +570,9 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): We define the amplitudes of the residual between the model and reference model for all cells as: .. math:: - \mathbf{\bar{m}} = \bigg ( - \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + - \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} The objective function for IRLS iteration :math:`k` is given by: @@ -622,9 +622,9 @@ def f_m(self, m): For smallness vector amplitude regularization, the regularization kernel function is: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( - \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + - \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} where the global set of model parameters :math:`\mathbf{m}` defined at cell centers is @@ -683,7 +683,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): function; with more blocky structures being recovered when a smaller norm is used. Optionally, custom cell weights can be included to control the degree of blockiness being enforced throughout different regions the model. - + See the *Notes* section below for a comprehensive description. Parameters @@ -746,7 +746,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): sparseness throughout the recovered model. Sharper boundaries are recovered in regions where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, the exponent becomes a constant. - + For implementation within SimPEG, the regularization function and its variables must be discretized onto a `mesh`. The discrete approximation for the regularization function (objective function) is expressed in linear form as: @@ -817,11 +817,11 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 where - + .. math:: - \mathbf{\bar{m}} = \bigg ( - \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + - \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the @@ -892,9 +892,9 @@ def f_m(self, m): (i.e. x-derivative), and .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( - \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + - \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} The global set of model parameters :math:`\mathbf{m}` defined at cell centers is @@ -928,11 +928,11 @@ def f_m_deriv(self, m) -> csr_matrix: regularization kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \begin{bmatrix} \mathbf{G_x} & \mathbf{0} \\ \mathbf{0} & \mathbf{G_x} \end{bmatrix} where :math:`\mathbf{G_x}` is the partial cell gradient operator along x - (i.e. the x-derivative). + (i.e. the x-derivative). Parameters ---------- @@ -1044,7 +1044,7 @@ class VectorAmplitude(Sparse): \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) \Bigg | \, \frac{\partial |\vec{m}|}{\partial \xi_j} \, \bigg |^{p_j(r)} \, dv - + where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, and :math:`w(r)` is a user-defined weighting function applied to all terms. :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. @@ -1071,7 +1071,7 @@ class VectorAmplitude(Sparse): | \, \vec{m}_i \, | \;\;\;\;\;\;\; (no \; reference \; model)\\ | \, \vec{m}_i - \vec{m}_i^{(ref)} \, | \;\;\;\; (reference \; model) \end{cases} - + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account for cell dimensions in the discretization and 2) apply user-defined weighting. :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). @@ -1091,7 +1091,7 @@ class VectorAmplitude(Sparse): r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: @@ -1109,9 +1109,9 @@ class VectorAmplitude(Sparse): where .. math:: - \Delta \mathbf{\bar{m}} = \bigg ( - \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + - \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Delta \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} and @@ -1163,11 +1163,11 @@ class VectorAmplitude(Sparse): >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) **Reference model in smoothness:** - + Gradients/interfaces within a discrete reference model can be preserved by including the reference model the smoothness regularization. In this case, the objective function becomes: - + .. math:: \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 From a3cc7c51afd41a3945ef5f48e846ec15b5fd0769 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 09:34:26 -0700 Subject: [PATCH 192/455] break these out of for loops --- SimPEG/meta/multiprocessing.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 0e82b27c18..fe70d702b5 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -207,12 +207,23 @@ def __init__(self, simulations, mappings, n_processes=None): print("chunk sizes:", chunk_sizes) - processes = [] + self._sim_processes = [] i_start = 0 chunk_nd = [] sim_futures = [] - for chunk in chunk_sizes: - print(f"chunking {chunk}.") + + for _ in range(len(chunk_sizes)): + print("creating process") + self._sim_processes.append(_SimulationProcess()) + print("processes created") + + for p in self._sim_processes: + print("starting process") + p.start() + print("processes started") + + for p, chunk in zip(self._sim_processes, chunk_sizes): + print(f"sending chunk {chunk}.") if chunk == 0: continue i_end = i_start + chunk @@ -220,18 +231,12 @@ def __init__(self, simulations, mappings, n_processes=None): self.simulations[i_start:i_end], self.mappings[i_start:i_end] ) chunk_nd.append(sim_chunk.survey.nD) - print("creating process") - p = _SimulationProcess() - processes.append(p) - print("starting process") - p.start() - print("started") + print("sending sim") p.set_sim(sim_chunk) print("sent sim") i_start = i_end self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) - self._sim_processes = processes @MetaSimulation.model.setter def model(self, value): From 9e28d33d37e10b961191938585fa1488c2366821 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:21:03 -0700 Subject: [PATCH 193/455] more prints --- tests/meta/test_multiprocessing_sim.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index ec8fb0994a..0608d70e4e 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -67,8 +67,11 @@ def test_meta_correctness(): print("test B") # test data output + print("serial") d_full = serial_sim.dpred(m_test, f=f_serial) + print("done") d_mult = parallel_sim.dpred(m_test, f=f_parallel) + print("parallel done") np.testing.assert_allclose(d_full, d_mult) print("test C") From 06c1c73cd86b9d4e3ab47126a4ce68f8a3cd2728 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:25:41 -0700 Subject: [PATCH 194/455] [no-ci] more prints --- SimPEG/meta/multiprocessing.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index fe70d702b5..0dae97ace4 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -87,25 +87,31 @@ def run(self): sim_key, f_key = args sim = _cached_items[sim_key] fields = _cached_items[f_key] - r_queue.put(sim.dpred(sim.model, fields)) + print("dpred?") + d_pred = sim.dpred(sim.model, fields) + print("dpred!") + r_queue.put(d_pred) elif op == 3: # do jvec sim_key, v, f_key = args sim = _cached_items[sim_key] fields = _cached_items[f_key] - r_queue.put(sim.Jvec(sim.model, v, fields)) + jvec = sim.Jvec(sim.model, v, fields) + r_queue.put(jvec) elif op == 4: # do jtvec sim_key, v, f_key = args sim = _cached_items[sim_key] fields = _cached_items[f_key] - r_queue.put(sim.Jtvec(sim.model, v, fields)) + jtvec = sim.Jtvec(sim.model, v, fields) + r_queue.put(jtvec) elif op == 5: # do jtj_diag sim_key, w, f_key = args sim = _cached_items[sim_key] fields = _cached_items[f_key] - r_queue.put(sim.getJtJdiag(sim.model, w, fields)) + jtj = sim.getJtJdiag(sim.model, w, fields) + r_queue.put(jtj) except Exception as err: r_queue.put(err) From 8c38f7763dc0441bea0c498e72580845d7857edb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:29:33 -0700 Subject: [PATCH 195/455] [no-ci] print cleanup --- SimPEG/meta/multiprocessing.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 0dae97ace4..4efc5cc879 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -80,7 +80,9 @@ def run(self): sim = _cached_items[sim_key] f_key = uuid.uuid4().hex r_queue.put(f_key) + print("fields?") fields = sim.fields(sim.model) + print("fields!") _cached_items[f_key] = fields elif op == 2: # do dpred @@ -201,35 +203,18 @@ def __init__(self, simulations, mappings, n_processes=None): if n_processes is None: n_processes = cpu_count() - print(f"starting with {n_processes} processes") # split simulation,mappings up into chunks # (Which are currently defined using MetaSimulations) n_sim = len(simulations) - print(n_sim) chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] for i in range(n_sim % n_processes): chunk_sizes[i] += 1 - print("chunk sizes:", chunk_sizes) - - self._sim_processes = [] i_start = 0 chunk_nd = [] - sim_futures = [] - - for _ in range(len(chunk_sizes)): - print("creating process") - self._sim_processes.append(_SimulationProcess()) - print("processes created") - - for p in self._sim_processes: - print("starting process") - p.start() - print("processes started") - + self._sim_processes = [] for p, chunk in zip(self._sim_processes, chunk_sizes): - print(f"sending chunk {chunk}.") if chunk == 0: continue i_end = i_start + chunk @@ -237,9 +222,12 @@ def __init__(self, simulations, mappings, n_processes=None): self.simulations[i_start:i_end], self.mappings[i_start:i_end] ) chunk_nd.append(sim_chunk.survey.nD) - print("sending sim") + + p = _SimulationProcess() + self._sim_processes.append(p) + p.start() p.set_sim(sim_chunk) - print("sent sim") + i_start = i_end self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) From 9cc8c17d73b621f51e327649d654f4e358c01e52 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:31:48 -0700 Subject: [PATCH 196/455] [no-ci] remove zip --- SimPEG/meta/multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 4efc5cc879..dca935d0b0 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -214,7 +214,7 @@ def __init__(self, simulations, mappings, n_processes=None): i_start = 0 chunk_nd = [] self._sim_processes = [] - for p, chunk in zip(self._sim_processes, chunk_sizes): + for chunk in chunk_sizes: if chunk == 0: continue i_end = i_start + chunk From 05cb3c6c9d65a2d4e3d149ab84a524cd824fdb30 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:35:50 -0700 Subject: [PATCH 197/455] [no-ci] change processes --- SimPEG/meta/multiprocessing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index dca935d0b0..c17ff32575 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -213,7 +213,7 @@ def __init__(self, simulations, mappings, n_processes=None): i_start = 0 chunk_nd = [] - self._sim_processes = [] + processes = [] for chunk in chunk_sizes: if chunk == 0: continue @@ -222,14 +222,13 @@ def __init__(self, simulations, mappings, n_processes=None): self.simulations[i_start:i_end], self.mappings[i_start:i_end] ) chunk_nd.append(sim_chunk.survey.nD) - p = _SimulationProcess() - self._sim_processes.append(p) + processes.append(p) p.start() p.set_sim(sim_chunk) - i_start = i_end + self._sim_processes = processes self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) @MetaSimulation.model.setter From 4b070f0997e29240a90366289af221e8c68c916c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 10:49:11 -0700 Subject: [PATCH 198/455] [no-ci] add timeout --- SimPEG/meta/multiprocessing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index c17ff32575..e34fc822c9 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -81,6 +81,9 @@ def run(self): f_key = uuid.uuid4().hex r_queue.put(f_key) print("fields?") + print(sim_key) + print(sim) + print(sim.model) fields = sim.fields(sim.model) print("fields!") _cached_items[f_key] = fields @@ -302,11 +305,11 @@ def getJtJdiag(self, m, W=None, f=None): self._jtjdiag = np.sum(jtj_diag, axis=0) return self._jtjdiag - def join(self): + def join(self, timeout=None): for p in self._sim_processes: if p.is_alive(): p.task_queue.put(None) - p.join() + p.join(timeout=timeout) class MultiprocessingSumMetaSimulation( From 324d33f8eded75ff699d9f1d81ebd7fde089c0fe Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 11:48:26 -0700 Subject: [PATCH 199/455] set mp start method on 3.8 to spawn --- tests/meta/test_multiprocessing_sim.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 0608d70e4e..29315e70b0 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -1,4 +1,6 @@ import numpy as np +import multiprocessing as mp +import sys from SimPEG.potential_fields import gravity from SimPEG.electromagnetics.static import resistivity as dc @@ -15,6 +17,9 @@ MultiprocessingRepeatedSimulation, ) +if sys.version[0] == 3 and sys.version_info[1] <= 8: + mp.set_start_method("spawn") + def test_meta_correctness(): print("starting_meta_test_correct") From e66f123206e1f2106c8c0d50892f0433e861b79f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 11:52:40 -0700 Subject: [PATCH 200/455] remove print statements --- SimPEG/meta/multiprocessing.py | 11 ----------- tests/meta/test_multiprocessing_sim.py | 26 +------------------------- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index e34fc822c9..07e8e054d4 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -80,21 +80,14 @@ def run(self): sim = _cached_items[sim_key] f_key = uuid.uuid4().hex r_queue.put(f_key) - print("fields?") - print(sim_key) - print(sim) - print(sim.model) fields = sim.fields(sim.model) - print("fields!") _cached_items[f_key] = fields elif op == 2: # do dpred sim_key, f_key = args sim = _cached_items[sim_key] fields = _cached_items[f_key] - print("dpred?") d_pred = sim.dpred(sim.model, fields) - print("dpred!") r_queue.put(d_pred) elif op == 3: # do jvec @@ -375,10 +368,6 @@ def __init__(self, simulation, mappings, n_processes=None): if n_processes is None: n_processes = cpu_count() - if n_processes is None: - n_processes = cpu_count() - print(f"starting with {n_processes} processes") - # split mappings up into chunks n_sim = len(mappings) chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index 29315e70b0..bc816b0e24 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -17,12 +17,11 @@ MultiprocessingRepeatedSimulation, ) -if sys.version[0] == 3 and sys.version_info[1] <= 8: +if sys.version_info[0] == 3 and sys.version_info[1] <= 8: mp.set_start_method("spawn") def test_meta_correctness(): - print("starting_meta_test_correct") mesh = TensorMesh([16, 16, 16], origin="CCN") rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] @@ -60,45 +59,34 @@ def test_meta_correctness(): dc_mappings.append(maps.IdentityMap()) serial_sim = MetaSimulation(dc_sims, dc_mappings) - print("created serial") parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=12) - print("created parallel") try: # create fields objects - print("test A") f_serial = serial_sim.fields(m_test) f_parallel = parallel_sim.fields(m_test) - print("test B") # test data output - print("serial") d_full = serial_sim.dpred(m_test, f=f_serial) - print("done") d_mult = parallel_sim.dpred(m_test, f=f_parallel) - print("parallel done") np.testing.assert_allclose(d_full, d_mult) - print("test C") # test Jvec u = np.random.rand(mesh.n_cells) jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) - print("test D") np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec v = np.random.rand(serial_sim.survey.nD) jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) - print("test E") np.testing.assert_allclose(jtvec_full, jtvec_mult) # test get diag diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) - print("test F") np.testing.assert_allclose(diag_full, diag_mult) @@ -106,22 +94,18 @@ def test_meta_correctness(): parallel_sim.model = m_test d_mult2 = parallel_sim.dpred() np.testing.assert_allclose(d_mult, d_mult2) - print("test g") jvec_mult2 = parallel_sim.Jvec(m_test, u) np.testing.assert_allclose(jvec_mult, jvec_mult2) - print("test h") jtvec_mult2 = parallel_sim.Jtvec(m_test, v) np.testing.assert_allclose(jtvec_mult, jtvec_mult2) - print("test i") # also pass a diagonal matrix here for testing. parallel_sim._jtjdiag = None W = sp.eye(parallel_sim.survey.nD) diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) np.testing.assert_allclose(diag_mult, diag_mult2) - print("test j") except Exception as err: raise err finally: @@ -129,7 +113,6 @@ def test_meta_correctness(): def test_sum_correctness(): - print("starting_meta_test_sum") mesh = TensorMesh([16, 16, 16], origin="CCN") # Create gravity sum sims rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T @@ -211,7 +194,6 @@ def test_sum_correctness(): def test_repeat_correctness(): - print("starting_meta_test_repeat") mesh = TensorMesh([16, 16, 16], origin="CCN") rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T rx = gravity.Point(rx_locs, components=["gz"]) @@ -238,36 +220,30 @@ def test_repeat_correctness(): try: # test field things - print("test 1") f_serial = serial_sim.fields(t_model) f_parallel = parallel_sim.fields(t_model) # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) - print("test 2") d_full = serial_sim.dpred(t_model, f_serial) d_repeat = parallel_sim.dpred(t_model, f_parallel) np.testing.assert_equal(d_full, d_repeat) - print("test 3") # test Jvec u = np.random.rand(len(t_model)) jvec_full = serial_sim.Jvec(t_model, u, f=f_serial) jvec_mult = parallel_sim.Jvec(t_model, u, f=f_parallel) np.testing.assert_allclose(jvec_full, jvec_mult) - print("test 4") # test Jtvec v = np.random.rand(len(sim_ts) * survey.nD) jtvec_full = serial_sim.Jtvec(t_model, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(t_model, v, f=f_parallel) np.testing.assert_allclose(jtvec_full, jtvec_mult) - print("test 5") # test get diag diag_full = serial_sim.getJtJdiag(t_model, f=f_serial) diag_mult = parallel_sim.getJtJdiag(t_model, f=f_parallel) np.testing.assert_allclose(diag_full, diag_mult) - print("test 6") except Exception as err: raise err finally: From acb960bac14685137967a69f654a6066cb0a7b97 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 11:58:24 -0700 Subject: [PATCH 201/455] add note about python 3.8 and unix 'fork' spawn method. --- SimPEG/meta/multiprocessing.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 07e8e054d4..80721c2a9f 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -192,6 +192,15 @@ class MultiprocessingMetaSimulation(MetaSimulation): any other multiprocessing queue. >>> sim.close() + + Notes + ----- + On Unix systems with python version 3.8 the default `fork` method of starting the + processes has lead to program stalls in certain cases. If you encounter this + try setting the start method to `spawn'. + + >>> import multiprocessing as mp + >>> mp.set_start_method("spawn") """ def __init__(self, simulations, mappings, n_processes=None): From e8194030dd61be4c78eac5c1514c5326d996ee78 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 12 Jun 2023 12:00:06 -0700 Subject: [PATCH 202/455] add back all tests --- .azure-pipelines/matrix.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index b1d59ce0e5..8f5afb8cb6 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -1,7 +1,17 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] - test: ['tests/meta'] + test: ['tests/em', + 'tests/base tests/flow tests/seis tests/utils tests/meta', + 'tests/docs -s -v', + 'tests/examples/test_examples_1.py', + 'tests/examples/test_examples_2.py', + 'tests/examples/test_examples_3.py', + 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', + 'tests/examples/test_tutorials_3.py', + 'tests/pf', + 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. + ] jobs: - ${{ each os in parameters.os }}: @@ -11,7 +21,7 @@ jobs: displayName: ${{ os }}_${{ py_vers }}_${{ test }} pool: vmImage: ${{ os }} - timeoutInMinutes: 30 + timeoutInMinutes: 120 steps: - script: | wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" @@ -37,7 +47,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test export KMP_WARNINGS=0 - pytest ${{ test }} -s -v --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning + pytest ${{ test }} --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning displayName: 'Testing ${{ test }}' - script: | From 09f12c61dbb6b6ffdae8dea95f84c6e727d3f395 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 13 Jun 2023 10:00:26 -0700 Subject: [PATCH 203/455] add style testing back --- azure-pipelines.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 57ba36ffe0..3e49e9954b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,7 +18,6 @@ stages: - stage: StyleChecks displayName: "Style Checks" - condition: False jobs: - job: displayName: Run style checks with Black @@ -61,7 +60,6 @@ stages: - stage: Testing dependsOn: StyleChecks - condition: True jobs: - template: ./.azure-pipelines/matrix.yml From 0cb6d00719d6ab3138b868fc43b7b23933bce918 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 13 Jun 2023 11:38:08 -0700 Subject: [PATCH 204/455] add some documentation --- SimPEG/meta/__init__.py | 8 +++- SimPEG/meta/multiprocessing.py | 77 +++++++++++++++++++++++++++++++++- SimPEG/meta/simulation.py | 7 ++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/SimPEG/meta/__init__.py b/SimPEG/meta/__init__.py index 30aa3553b1..e60961e273 100644 --- a/SimPEG/meta/__init__.py +++ b/SimPEG/meta/__init__.py @@ -40,7 +40,13 @@ Multiprocessing --------------- -Coming soon! + +.. autosummary:: + :toctree: generated/ + + MultiprocessingMetaSimulation + MultiprocessingSumMetaSimulation + MultiprocessingRepeatedSimulation Dask ---- diff --git a/SimPEG/meta/multiprocessing.py b/SimPEG/meta/multiprocessing.py index 80721c2a9f..bc3708d57a 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/SimPEG/meta/multiprocessing.py @@ -6,6 +6,8 @@ class SimpleFuture: + """Represents an object stored on a seperate simulation process.""" + def __init__(self, item_id, t_queue, r_queue): self.item_id = item_id self.t_queue = t_queue @@ -172,7 +174,11 @@ class MultiprocessingMetaSimulation(MetaSimulation): """Multiprocessing version of simulation of simulations. This class makes use of the `multiprocessing` module to provide - concurrency, executing the internal simulations in parallel. + concurrency, executing the internal simulations in parallel. This class + is meant to be a (mostly) drop in replacement for :class:`.MetaSimulation`. + If you want to test your implementation, we recommend starting with a + small problem using `MetaSimulation`, then switching it to this class. + the serial version of this class is good for testing correctness. If using this class, please be conscious of your operating system's default method of spawning new processes. On Windows systems this @@ -193,6 +199,20 @@ class MultiprocessingMetaSimulation(MetaSimulation): >>> sim.close() + Parameters + ---------- + simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of SimPEG.maps.IdentityMap + The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + n_processes : optional + The number of processes to spawn internally. This will default + to `multiprocessing.cpu_count()`. The number of processes spawned + will be the minimum of this number and the number of simulations. + Notes ----- On Unix systems with python version 3.8 the default `fork` method of starting the @@ -245,6 +265,20 @@ def model(self, value): p.store_model(self._model) def fields(self, m): + """Create fields for every simulation. + + The returned list contains the field object from each simulation. + + Parameters + ---------- + m : array_like + The full model vector. + + Returns + ------- + (n_sim) list of SimpleFuture + The list of references to the fields stored on the separate processes. + """ self.model = m # The above should pass the model to all the internal simulations. f = [] @@ -317,6 +351,26 @@ def join(self, timeout=None): class MultiprocessingSumMetaSimulation( MultiprocessingMetaSimulation, SumMetaSimulation ): + """A multiprocessing version of :class:`.SumMetaSimulation`. + + See the documentation of :class:`.MultiprocessingMetaSimulation` for + details on how to use multiprocessing for you operating system. + + Parameters + ---------- + simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of SimPEG.maps.IdentityMap + The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + n_processes : optional + The number of processes to spawn internally. This will default + to `multiprocessing.cpu_count()`. The number of processes spawned + will be the minimum of this number and the number of simulations. + """ + def dpred(self, m=None, f=None): if f is None: if m is None: @@ -370,6 +424,27 @@ def getJtJdiag(self, m, W=None, f=None): class MultiprocessingRepeatedSimulation( MultiprocessingMetaSimulation, RepeatedSimulation ): + """A multiprocessing version of the :class:`.RepeatedSimulation`. + + This class makes use of a single simulation that is copied to each internal + process, but only once per process. + + This simulation shares internals with the :class:`.MultiprocessingMetaSimulation`. + class, as such please see that documentation for details regarding how to properly + use multiprocessing on your operating system. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseSimulation + The simulation to use repeatedly with different mappings. + mappings : (n_sim) list of SimPEG.maps.IdentityMap + The list of different mappings to use. + n_processes : optional + The number of processes to spawn internally. This will default + to `multiprocessing.cpu_count()`. The number of processes spawned + will be the minimum of this number and the number of simulations. + """ + def __init__(self, simulation, mappings, n_processes=None): # do this to call the initializer of the Repeated Sim super(MultiprocessingMetaSimulation, self).__init__(simulation, mappings) diff --git a/SimPEG/meta/simulation.py b/SimPEG/meta/simulation.py index d5a94846f8..a2327b9606 100644 --- a/SimPEG/meta/simulation.py +++ b/SimPEG/meta/simulation.py @@ -193,6 +193,11 @@ def fields(self, m): The returned list contains the field object from each simulation. + Parameters + ---------- + m : array_like + The full model vector. + Returns ------- (n_sim) list @@ -422,7 +427,9 @@ class RepeatedSimulation(MetaSimulation): Parameters ---------- simulation : SimPEG.simulation.BaseSimulation + The simulation to use repeatedly with different mappings. mappings : (n_sim) list of SimPEG.maps.IdentityMap + The list of different mappings to use. """ _repeat_sim = True From 4fda4e23ef9bb6d5ccec319e187c60d7799f4539 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 14 Jun 2023 15:58:13 -0700 Subject: [PATCH 205/455] cleanup based on linter --- SimPEG/electromagnetics/frequency_domain/simulation.py | 1 - tests/em/fdem/forward/test_permittivity.py | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index e72a3285df..a94350056b 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -1,6 +1,5 @@ import numpy as np import scipy.sparse as sp -from scipy.constants import epsilon_0 from discretize.utils import Zero from ... import props diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index cd11b152d2..79561a4123 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -7,8 +7,6 @@ import discretize from discretize import utils from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.electromagnetics import resistivity as dc -from SimPEG import utils, maps, Report from pymatsolver import Pardiso @@ -282,14 +280,12 @@ def test_cross_check_e_dipole(epsilon_r, frequency): name1="J-formulation", name2="H-formulation", ) - assert np.all(h_comparison) + assert np.all(b_comparison) @pytest.mark.parametrize("epsilon_r", epsilon_r_list) @pytest.mark.parametrize("frequency", frequency_list) def test_cross_check_b_dipole(epsilon_r, frequency): - tolerance = 1e-8 - sigma_back = 1e-2 epsilon_r_back = 1 @@ -374,4 +370,4 @@ def test_cross_check_b_dipole(epsilon_r, frequency): name1="B-formulation", name2="E-formulation", ) - assert np.all(e_comparison) + assert np.all(j_comparison) From b5cbe5f3917bab6f58174415fd0398bf07977e19 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 14 Jun 2023 16:20:29 -0700 Subject: [PATCH 206/455] more linter cleanup --- tests/em/fdem/forward/test_permittivity.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index 79561a4123..a7d5f61fc1 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -5,7 +5,6 @@ import geoana import discretize -from discretize import utils from SimPEG.electromagnetics import frequency_domain as fdem from pymatsolver import Pardiso @@ -190,8 +189,6 @@ def test_e_dipole(epsilon, frequency, simulation): @pytest.mark.parametrize("epsilon_r", epsilon_r_list) @pytest.mark.parametrize("frequency", frequency_list) def test_cross_check_e_dipole(epsilon_r, frequency): - tolerance = 1e-8 - sigma_back = 1e-2 epsilon_r_back = 1 From 86a149770fdf1e3757c9a9bcc218651d8f286389 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 15 Jun 2023 11:20:22 -0700 Subject: [PATCH 207/455] Move flake8 ignores to .flake8 Moves the flake rules we are currently ignoring to the .flake8 configuration file. Rename the Makefile targets: `make flake` runs flake8 with ignores set in `.flake8`, while `make flake-full` runs flake8 with the full set of rules (without ignoring the ones we aren't following at the moment). Add a `help` target to the `Makefile` that prints out description of the available targets. --- .flake8 | 108 ++++++++++++++++++++++++++++++++++++++++++++ Makefile | 76 +++++-------------------------- azure-pipelines.yml | 2 +- 3 files changed, 120 insertions(+), 66 deletions(-) diff --git a/.flake8 b/.flake8 index b770d960c0..6fb1cf9ed9 100644 --- a/.flake8 +++ b/.flake8 @@ -6,6 +6,8 @@ # ---------------- [flake8] extend-ignore = + # Default ignores by flake (added here for when ignore gets overwritten) + E121,E123,E126,E226,E24,E704,W503,W504, # Too many leading '#' for block comment E266, # Line too long (82 > 79 characters) @@ -26,6 +28,112 @@ per-file-ignores = exclude-from-doctest = # Don't check style in docstring of test functions tests +# Define flake rules that will be ignored for now. Every time a new warning is +# solved througout the entire project, it should be removed to this list. +ignore = + # assertRaises(Exception): should be considered evil + B017, + # No explicit stacklevel argument found. + B028, + # Missing docstring in public module + D100, + # Missing docstring in public class + D101, + # Missing docstring in public method + D102, + # Missing docstring in public function + D103, + # Missing docstring in public package + D104, + # Missing docstring in magic method + D105, + # Missing docstring in __init__ + D107, + # One-line docstring should fit on one line with quotes + D200, + # No blank lines allowed before function docstring + D201, + # No blank lines allowed after function docstring + D202, + # 1 blank line required between summary line and description + D205, + # Docstring is over-indented + D208, + # Multi-line docstring closing quotes should be on a separate line + D209, + # No whitespaces allowed surrounding docstring text + D210, + # No blank lines allowed before class docstring + D211, + # Use """triple double quotes""" + D300, + # First line should end with a period + D400, + # First line should be in imperative mood; try rephrasing + D401, + # First line should not be the function's "signature" + D402, + # First word of the first line should be properly capitalized + D403, + # No blank lines allowed between a section header and its content + D412, + # Section has no content + D414, + # Docstring is empty + D419, + # module level import not at top of file + E402, + # comparison to None should be ‘if cond is None:’ + E711, + # do not assign a lambda expression, use a def + E731, + # 'from %s import *' used; unable to detect undefined names + F403, + # %r may be undefined, or defined from star imports: %s + F405, + # '...'.format(...) has unused named argument(s): %s + F522, + # '...'.format(...) has unused arguments at position(s): %s + F523, + # '...'.format(...) is missing argument(s) for placeholder(s): %s + F524, + # f-string is missing placeholders + F541, + # redefinition of unused %r from line %r + F811, + # undefined name %r + F821, + # Block quote ends without a blank line; unexpected unindent. + RST201, + # Definition list ends without a blank line; unexpected unindent. + RST203, + # Field list ends without a blank line; unexpected unindent. + RST206, + # Inline strong start-string without end-string. + RST210, + # Title underline too short. + RST212, + # Inline emphasis start-string without end-string. + RST213, + # Inline interpreted text or phrase reference start-string without end-string. + RST215, + # Inline substitution_reference start-string without end-string. + RST219, + # Unexpected indentation. + RST301, + # Unknown directive type "*". + RST303, + # Unknown interpreted text role "*". + RST304, + # Error in "*" directive: + RST307, + # Previously unseen severe error, not yet assigned a unique code. + RST499, + # trailing whitespace + W291, + # blank line contains whitespace + W293, + # Configure flake8-rst-docstrings # ------------------------------- diff --git a/Makefile b/Makefile index 958d76e4a6..3caa5706a3 100644 --- a/Makefile +++ b/Makefile @@ -1,69 +1,15 @@ STYLE_CHECK_FILES = SimPEG examples tutorials tests -# Define flake8 warnings that shouldn't be catched for now. -# Every time a new warning is solved througout the entire project, it should be -# removed to this list. -# This list is only meant to be used in the flake-permissive target and it's -# a temprary solution until every flake8 warning is solved in SimPEG. -# The first set of rules (up to W504) are the default ignored ones by flake8. -# Since we are using the --ignore option we are overriding them. They are -# included in the list so we keep ignoring them while running the -# flake-permissive target. -FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ - B017,\ - B028,\ - D100,\ - D101,\ - D102,\ - D103,\ - D104,\ - D105,\ - D107,\ - D200,\ - D201,\ - D202,\ - D205,\ - D208,\ - D209,\ - D210,\ - D211,\ - D300,\ - D400,\ - D401,\ - D402,\ - D403,\ - D412,\ - D414,\ - D419,\ - E402,\ - E711,\ - E731,\ - F403,\ - F405,\ - F522,\ - F523,\ - F524,\ - F541,\ - F811,\ - F821,\ - RST201,\ - RST203,\ - RST206,\ - RST210,\ - RST212,\ - RST213,\ - RST215,\ - RST219,\ - RST301,\ - RST303,\ - RST304,\ - RST307,\ - RST499,\ - W291,\ - W293,\ -" +.PHONY: help build coverage lint graphs tests docs check black flake -.PHONY: build coverage lint graphs tests docs check black flake +help: + @echo "Commands:" + @echo "" + @echo " check run code style and quality checks (black and flake8)" + @echo " black checks code style with black" + @echo " flake checks code style with flake8" + @echo " flake-full checks code style with flake8 (full set of rules)" + @echo "" build: python setup.py build_ext --inplace @@ -98,6 +44,6 @@ flake: flake8 --version flake8 ${FLAKE8_OPTS} ${STYLE_CHECK_FILES} -flake-permissive: +flake-all: flake8 --version - flake8 ${FLAKE8_OPTS} --ignore ${FLAKE8_IGNORE} ${STYLE_CHECK_FILES} + flake8 ${FLAKE8_OPTS} --ignore "" ${STYLE_CHECK_FILES} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e49e9954b..df14fadbb3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,7 +42,7 @@ stages: - script: | pip install -r requirements_style.txt displayName: "Install dependencies to run the checks" - - script: make flake-permissive + - script: make flake displayName: "Run flake8" - job: From 0041b793c1746b1a1dd73434dcfc269c0072e5f9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 15 Jun 2023 12:11:27 -0700 Subject: [PATCH 208/455] Exclude setup.py and docs/conf.py in flake8 runs --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index 6fb1cf9ed9..b0d4d63536 100644 --- a/.flake8 +++ b/.flake8 @@ -22,6 +22,8 @@ exclude = .git, __pycache__, .ipynb_checkpoints, + setup.py + docs/conf.py per-file-ignores = # disable unused-imports errors on __init__.py __init__.py: F401 From e90708eed4b46649340d13d49ff7f68b5a1e84fe Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 15 Jun 2023 12:18:07 -0700 Subject: [PATCH 209/455] Add flake8 to pre-commit configuration Motivated by https://github.com/simpeg/discretize/commit/8f56b21c823aec49f2d8a43d2d621ecba56a38c0 --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42cdea4343..ebc8cdb588 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,3 +4,9 @@ repos: hooks: - id: black language_version: python3.10 + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + language_version: python3.10 + additional_dependencies: [flake8-bugbear, flake8-builtins, flake8-mutable, flake8-rst-docstrings, flake8-docstrings] From 87646e2124634ee206219eb5f7a3a60a6013d36b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 16 Jun 2023 09:36:20 -0700 Subject: [PATCH 210/455] Fix target name in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3caa5706a3..f29f7cbd3c 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: @echo " check run code style and quality checks (black and flake8)" @echo " black checks code style with black" @echo " flake checks code style with flake8" - @echo " flake-full checks code style with flake8 (full set of rules)" + @echo " flake-all checks code style with flake8 (full set of rules)" @echo "" build: From d759b4c7e8a9eae226f5dcf460ff2c2cd4040ac3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 16 Jun 2023 09:36:57 -0700 Subject: [PATCH 211/455] Update Azure job that runs flake-all --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index df14fadbb3..6456e91c9b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,7 +55,7 @@ stages: - script: | pip install -r requirements_style.txt displayName: "Install dependencies to run the checks" - - script: FLAKE8_OPTS="--exit-zero" make flake + - script: FLAKE8_OPTS="--exit-zero" make flake-all displayName: "Run flake8" - stage: Testing From 9af270979a1d184203ad4ef52686c58f2e6190bb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 16 Jun 2023 13:47:26 -0700 Subject: [PATCH 212/455] Add pre-commit to environment.yml --- environment_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment_test.yml b/environment_test.yml index 59d59bc19f..4eee274437 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -36,6 +36,7 @@ dependencies: - pip - python-kaleido # Linters and code style + - pre-commit - black==23.1.0 - flake8 - flake8-bugbear From eacc94ea9b3182a8dab2cfd3a3bf18dc65146a1b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 16 Jun 2023 13:48:45 -0700 Subject: [PATCH 213/455] Split contributing guides into separate sections Merge the "Getting started: for Developers", "Practices" and "Contributing to SimPEG" into a single "Contributing to SimPEG" section, that contains pages for each topic. Add `pre-commit` to the `environment_test.yml`. Add instructions for setting up `pre-commit` in SimPEG repo, and to run Black and flake8 manually. --- docs/content/basic/contributing.rst | 1 - docs/content/basic/contributing/advanced.rst | 23 ++ .../content/basic/contributing/code-style.rst | 39 +++ .../basic/contributing/documentation.rst | 66 +++++ docs/content/basic/contributing/index.rst | 101 +++++++ .../basic/contributing/pull-requests.rst | 35 +++ .../contributing/setting-up-environment.rst | 108 +++++++ docs/content/basic/contributing/testing.rst | 146 ++++++++++ .../contributing/working-with-github.rst | 50 ++++ docs/content/basic/installing.rst | 28 +- .../basic/installing_for_developers.rst | 180 ------------ docs/content/basic/practices.rst | 267 ------------------ docs/content/getting_started.rst | 4 +- 13 files changed, 589 insertions(+), 459 deletions(-) delete mode 100644 docs/content/basic/contributing.rst create mode 100644 docs/content/basic/contributing/advanced.rst create mode 100644 docs/content/basic/contributing/code-style.rst create mode 100644 docs/content/basic/contributing/documentation.rst create mode 100644 docs/content/basic/contributing/index.rst create mode 100644 docs/content/basic/contributing/pull-requests.rst create mode 100644 docs/content/basic/contributing/setting-up-environment.rst create mode 100644 docs/content/basic/contributing/testing.rst create mode 100644 docs/content/basic/contributing/working-with-github.rst delete mode 100644 docs/content/basic/installing_for_developers.rst delete mode 100644 docs/content/basic/practices.rst diff --git a/docs/content/basic/contributing.rst b/docs/content/basic/contributing.rst deleted file mode 100644 index b1cd2f37dc..0000000000 --- a/docs/content/basic/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../../CONTRIBUTING.rst diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/basic/contributing/advanced.rst new file mode 100644 index 0000000000..2fcf5531b6 --- /dev/null +++ b/docs/content/basic/contributing/advanced.rst @@ -0,0 +1,23 @@ +.. _advanced: + +Advanced: Installing Solvers +---------------------------- + +Pardiso_ is a direct solvers that can be used for solving large(ish) +linear systems of equations. The provided testing environment should install +the necessary solvers for you. pymatsolver_ If you wish to modify pymatsolver_ as well +follow the instructions to download and install pymatsolver_. + +.. _Pardiso: https://www.pardiso-project.org + +.. _pymatsolver: https://github.com/rowanc1/pymatsolver + +If you open a `Jupyter notebook`_ and are able to run:: + + from pymatsolver import Pardiso + +.. _Jupyter notebook: http://jupyter.org/ + +then you have succeeded! Otherwise, make an `issue in pymatsolver`_. + +.. _issue in pymatsolver: https://github.com/rowanc1/pymatsolver/issues diff --git a/docs/content/basic/contributing/code-style.rst b/docs/content/basic/contributing/code-style.rst new file mode 100644 index 0000000000..21ff034fc0 --- /dev/null +++ b/docs/content/basic/contributing/code-style.rst @@ -0,0 +1,39 @@ +.. _code-style: + +Code style +========== + +Consistency makes code more readable and easier for collaborators to jump in. +SimPEG uses Black_ to autoformat its codebase, and flake8_ to lint its code and +enforce style rules. Black_ can automatically format SimPEG's codebase to +ensure it complies with Black code style. flake8_ performs style checks, raises +warnings on code that could lead towards bugs, performs checks on consistent +documentation formatting, and identify poor coding practices. + +.. important:: + + If you :ref:`configure-pre-commit`, pre-commit_ will automatically run + Black and flake8 on every commit. + +Alternatively, one could run them manually at anytime. +Run ``black`` on SimPEG directories that contain Python source files: + +.. code:: + + black SimPEG examples tutorials tests + +Run ``flake8`` on the whole project with: + +.. code:: + + flake8 + +.. note:: + + SimPEG is currently not `PEP 8 `_ + compliant and is not following all flake8 rules, but we are working towards + it and would appreciate contributions that do too! + +.. _Black: https://black.readthedocs.io/ +.. _flake8: https://flake8.pycqa.org/ +.. _pre-commit: https://pre-commit.com/ diff --git a/docs/content/basic/contributing/documentation.rst b/docs/content/basic/contributing/documentation.rst new file mode 100644 index 0000000000..b9a6df7b91 --- /dev/null +++ b/docs/content/basic/contributing/documentation.rst @@ -0,0 +1,66 @@ +.. _documentation: + +Documentation +------------- + +Documentation helps others use your code! Please document new contributions. +SimPEG tries to follow the `numpydoc` style of docstrings (check out the +`style guide `_). +SimPEG then uses `sphinx `_ to build the documentation. +When documenting a new class or function, please include a description +(with math if it solves an equation), inputs, outputs and preferably a small example. + +For example: + +.. code:: python + + + class WeightedLeastSquares(BaseComboRegularization): + r"""Weighted least squares measure on model smallness and smoothness. + + L2 regularization with both smallness and smoothness (first order + derivative) contributions. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh on which the model parameters are defined. This is used + for constructing difference operators for the smoothness terms. + active_cells : array_like of bool or int, optional + List of active cell indices, or a `mesh.n_cells` boolean array + describing active cells. + alpha_s : float, optional + Smallness weight + alpha_x, alpha_y, alpha_z : float or None, optional + First order smoothness weights for the respective dimensions. + `None` implies setting these weights using the `length_scale` + parameters. + alpha_xx, alpha_yy, alpha_zz : float, optional + Second order smoothness weights for the respective dimensions. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + mapping : SimPEG.maps.IdentityMap, optional + A mapping to apply to the model before regularization. + reference_model : array_like, optional + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + weights : None, array_like, or dict or array_like, optional + User defined weights. It is recommended to interact with weights using + the `get_weights`, `set_weights` functionality. + + Notes + ----- + The function defined here approximates: + + .. math:: + \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + Note if the key word argument `reference_model_in_smooth` is False, then mref is not + included in the smoothness contribution. + + If length scales are used to set the smoothness weights, alphas are respectively set internally using: + >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 + """ diff --git a/docs/content/basic/contributing/index.rst b/docs/content/basic/contributing/index.rst new file mode 100644 index 0000000000..f5b9c20ec5 --- /dev/null +++ b/docs/content/basic/contributing/index.rst @@ -0,0 +1,101 @@ +.. _contributing: + +Contributing to SimPEG +======================= + +First of all, we are glad you are here! We welcome contributions and input +from the community. + +In this pages we set some guidelines for contributing to the repositories +hosted in the `SimPEG `_ organization on GitHub. +These repositories are maintained on a volunteer basis. + +Please, be considerate and respectful of others. Remember that everyone in +the SimPEG community must abide by our `Code of +Conduct `_. + +Ask questions +------------- + +If you have a question regarding a specific use of SimPEG, the fastest way +to get a response is by posting on our Discourse discussion forum: +https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, +you can join our slack group at http://slack.simpeg.xyz. +Please do not create an issue to ask a question. + +.. _issues: + +GitHub Issues +------------- + +Issues are a place for you to suggest enhancements or raise problems you are +having with the package to the developers. Feel free to open new +`Issues in SimPEG's GitHub repository +`_. + +.. _bugs: + +Bugs +~~~~ + +If you found a bug in SimPEG, please report it through a +`new Issue `_. +Please be as descriptive and provide sufficient detail to reproduce the error. +Whenever possible, if you can include a small example that produces the error, +this will help us resolve issues faster. + +.. _suggest-enhancements: + +Suggesting enhancements +~~~~~~~~~~~~~~~~~~~~~~~ + +We welcome ideas for improvements on SimPEG! When writing an issue to suggest +an improvement, please + +- use a descriptive title, +- explain where the gap in current functionality is, +- include a pseudocode sketch of the intended functionality + +We will use the issue as a place to discuss and provide feedback. Please +remember that SimPEG is maintained on a volunteer basis. If you suggest an +enhancement, we certainly appreciate if you are also willing to take action +and start a Pull Request! + + +Contributing with code +---------------------- + +We are glad to receive contributions in form of code to SimPEG, weather it +fixes a bug, adds a new feature, improves the documentation or extends our +tests. In the following pages you'll find information on how to get started to +contribute with new code to SimPEG. + +* :ref:`working-with-github` +* :ref:`setting-up-environment` +* :ref:`code-style` +* :ref:`documentation` +* :ref:`testing` +* :ref:`pull-requests` +* :ref:`advanced` + +.. toctree:: + :hidden: + + working-with-github + setting-up-environment + code-style + documentation + testing + pull-requests + advanced + + +Licensing +~~~~~~~~~ + +All code contributed to SimPEG is licensed under the `MIT license +`_ which allows open +and commercial use and extension of SimPEG. If you did not write +the code yourself, it is your responsibility to ensure that the existing +license is compatible and included in the contributed files or you can obtain +permission from the original author to relicense the code. diff --git a/docs/content/basic/contributing/pull-requests.rst b/docs/content/basic/contributing/pull-requests.rst new file mode 100644 index 0000000000..4475ae1828 --- /dev/null +++ b/docs/content/basic/contributing/pull-requests.rst @@ -0,0 +1,35 @@ +.. _pull-requests: + +Pull Requests +============= + +We welcome contributions to SimPEG in the form of pull requests (PR). + +Stages of a pull request +------------------------ + +When first creating a pull request (PR), try to make your suggested changes as +tightly scoped as possible (try to solve one problem at a time). The fewer +changes you make, the faster your branch will be merged! + +If your pull request is not ready for final review, but you still want feedback +on your current coding process please mark it as a draft pull request. Once you +feel the pull request is ready for final review, you can convert the draft PR to +an open PR by selecting the ``Ready for review`` button at the bottom of the page. + +Once a pull request is in ``open`` status and you are ready for review, please +ping the simpeg developers in a github comment ``@simpeg/simpeg-developers`` to +request a review. At minimum for a PR to be eligible to merge, we look for + +- 100% (or as close as possible) difference testing. Meaning any new code is + completely tested. +- All tests are passing. +- All reviewer comments (if any) have been addressed. +- A developer approves the PR. + +After all these steps are satisfied, a ``@simpeg/simpeg-admin`` will merge your +pull request into the main branch (feel free to ping one of us on Github). + +This being said, all SimPEG developers and admins are essentially volunteers +providing their time for the benefit of the community. This does mean that +it might take some time for us to get your PR. diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst new file mode 100644 index 0000000000..7f38cf6b3a --- /dev/null +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -0,0 +1,108 @@ +.. _setting-up-environment: + +Setting up your environment +=========================== + +Install Python +-------------- + +First you will need to install Python. You can find instructions in +:ref:`installing_python`. We highly encourage to install Anaconda_ or +Mambaforge_. + +Create environment +------------------ + +To get started developing SimPEG we recommend setting up an environment using +the ``conda`` ( or ``mamba``) package manager that mimics the testing +environment used for continuous integration testing. Most of the packages that +we use are available through the ``conda-forge`` project. This will ensure you +have all of the necessary packages to both develop SimPEG and run tests +locally. We provide an ``environment_test.yml`` in the base level directory. + +.. code:: + + conda env create -f environment_test.yml + +.. note:: + If you find yourself wanting a faster package manager than ``conda`` + check out the ``mamba`` project at https://mamba.readthedocs.io/. It + usually is able to set up environments much quicker than ``conda`` and + can be used as a drop-in replacement (i.e. replace ``conda`` commands with + ``mamba``). + +Once the environment is successfully created, you can *activate* it with + +.. code:: + + conda activate simpeg-test + + +Install SimPEG in developer mode +-------------------------------- + +There are many options to install SimPEG into this local environment, we +recommend using `pip`. After ensuring that all necessary packages from +`environment_test.yml` are installed, the most robust command you can use, +executed from the base level directory would be :: + + pip install --no-deps -e . + +This is called an editable mode install (`-e`). This will make a symbolic link +for you to the working ``simpeg`` directory for that Python environment to use +and you can then make use of any changes you have made to the repository +without re-installing it. This command (`--no-deps`) also ensures pip won't +unintentionally re-install a package that was previously installed with conda. +This practice also allows you to uninstall SimPEG if so desired :: + + pip uninstall SimPEG + +.. note:: + + We no longer recommend modifying your Python path environment variable as + a way to install SimPEG for developers. + +.. _Anaconda: https://www.anaconda.com/products/individual +.. _Mambaforge: https://www.anaconda.com/products/individual + +Check your installation +----------------------- + +You should be able to open a terminal within SimPEG/tutorials and run an +example, ie. + +.. code:: + + python 02-linear_inversion/plot_inv_1_inversion_lsq.py + +or you can download and run the :ref:`notebook from the docs +`. + +.. image:: ../../tutorials/02-linear_inversion/images/sphx_glr_plot_inv_1_inversion_lsq_003.png + +You are now set up to SimPEG! + +.. note:: + + If all is not well, please submit an issue_ and `change this file`_! + +.. _issue: https://github.com/simpeg/simpeg/issues +.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/basic/contributing/setting-up-environment.rst + + +.. _configure-pre-commit: + +Configure pre-commit +-------------------- + +We recommend using pre-commit_ to ensure that your new code follows the code +style of SimPEG. pre-commit will run Black_ and flake8_ before any commit you +make. To configure it, you need to navigate to your cloned SimPEG repo and run: + +.. code:: + + pre-commit install + +.. _pre-commit: https://pre-commit.com/ +.. _Black: https://black.readthedocs.io +.. _flake8: https://flake8.pycqa.org diff --git a/docs/content/basic/contributing/testing.rst b/docs/content/basic/contributing/testing.rst new file mode 100644 index 0000000000..0d5e6abefb --- /dev/null +++ b/docs/content/basic/contributing/testing.rst @@ -0,0 +1,146 @@ +.. _testing: + +Testing +======= + +.. image:: https://dev.azure.com/simpeg/simpeg/_apis/build/status/simpeg.simpeg?branchName=main + :target: https://dev.azure.com/simpeg/simpeg/_build/latest?definitionId=2&branchName=main + :alt: Azure pipeline + +.. image:: https://codecov.io/gh/simpeg/simpeg/branch/main/graph/badge.svg + :target: https://codecov.io/gh/simpeg/simpeg + :alt: Coverage status + +On each update, SimPEG is tested using the continuous integration service +`Azure pipelines `_. +We use `Codecov `_ to check and provide stats on how much +of the code base is covered by tests. This tells which lines of code have been +run in the test suite. It does not tell you about the quality of the tests run! +In order to assess that, have a look at the tests we are running - they tell you +the assumptions that we do not want to break within the code base. + +Within the repository, the tests are located in the top-level **tests** +directory. Tests are organized similar to the structure of the repository. +There are several types of tests we employ, this is not an exhaustive list, +but meant to provide a few places to look when you are developing and would +like to check that the code you wrote satisfies the assumptions you think it +should. + +Testing is performed with :code:`pytest` which is available through PyPI. +Checkout the docs on `pytest `_. + + +Compare with known values +------------------------- + +In a simple case, you might know the exact value of what the output should be +and you can :code:`assert` that this is in fact the case. For example, +we setup a 3D :code:`BaseRectangularMesh` and assert that it has 3 dimensions. + +.. code:: python + + from discretize.base import BaseRectangularMesh + import numpy as np + + mesh = BaseRectangularMesh([6, 2, 3]) + + def test_mesh_dimensions(): + assert mesh.dim == 3 + +All functions with the naming convention :code:`test_XXX` +are run. Here we check that the dimensions are correct for the 3D mesh. + +If the value is not an integer, you can be subject to floating point errors, +so :code:`assert ==` might be too harsh. In this case, you will want to use +the ``numpy.testing`` module to check for approximate equals. For instance, + +.. code:: python + + import numpy as np + import discretize + from SimPEG import maps + + def test_map_multiplication(self): + mesh = discretize.TensorMesh([2,3]) + exp_map = maps.ExpMap(mesh) + vert_map = maps.SurjectVertical1D(mesh) + combo = exp_map*vert_map + m = np.arange(3.0) + t_true = np.exp(np.r_[0,0,1,1,2,2.]) + np.testing.assert_allclose(combo * m, t_true) + +These are rather simple examples, more advanced tests might include `solving an +electromagnetic problem numerically and comparing it to an analytical solution +`_ +, or `performing an adjoint test +`_ +to test :code:`Jvec` and :code:`Jtvec`. + + +.. _order_test: + +Order and Derivative Tests +-------------------------- + +Order tests can be used when you are testing differential operators (we are +using a second-order, staggered grid discretization for our operators). For +example, testing a 2D curl operator in `test_operators.py +`_ + +.. code:: python + + import numpy as np + import unittest + from discretize.tests import OrderTest + + class TestCurl2D(OrderTest): + name = "Cell Grad 2D - Dirichlet" + meshTypes = ['uniformTensorMesh'] + meshDimension = 2 + meshSizes = [8, 16, 32, 64] + + def getError(self): + # Test function + ex = lambda x, y: np.cos(y) + ey = lambda x, y: np.cos(x) + sol = lambda x, y: -np.sin(x)+np.sin(y) + + sol_curl2d = call2(sol, self.M.gridCC) + Ec = cartE2(self.M, ex, ey) + sol_ana = self.M.edge_curl*self.M.project_face_vector(Ec) + err = np.linalg.norm((sol_curl2d-sol_ana), np.inf) + + return err + + def test_order(self): + self.orderTest() + +Derivative tests are a particular type or :ref:`order_test`, and since they +are used so extensively, SimPEG includes a :code:`check_derivative` method. + +In the case +of testing a derivative, we consider a Taylor expansion of a function about +:math:`x`. For a small perturbation :math:`\Delta x`, + +.. math:: + + f(x + \Delta x) \simeq f(x) + J(x) \Delta x + \mathcal{O}(h^2) + +As :math:`\Delta x` decreases, we expect :math:`\|f(x) - f(x + \Delta x)\|` to +have first order convergence (e.g. the improvement in the approximation is +directly related to how small :math:`\Delta x` is, while if we include the +first derivative in our approximation, we expect that :math:`\|f(x) + +J(x)\Delta x - f(x + \Delta x)\|` to converge at a second-order rate. For +example, all `maps have an associated derivative test `_ . An example from `test_FDEM_derivs.py `_ + +.. code:: python + + def deriv_test(fdemType, comp): + + # setup simulation, survey + + def fun(x): + return survey.dpred(x), lambda x: sim.Jvec(x0, x) + return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) diff --git a/docs/content/basic/contributing/working-with-github.rst b/docs/content/basic/contributing/working-with-github.rst new file mode 100644 index 0000000000..e7a05ddbc6 --- /dev/null +++ b/docs/content/basic/contributing/working-with-github.rst @@ -0,0 +1,50 @@ +.. _working-with-github: + +Working with Git and GitHub +--------------------------- + +.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png + :align: right + :width: 100 + :target: https://github.com + + +To keep track of your code changes and contribute back to SimPEG, you will +need a Github_ account. Then fork the `SimPEG repository +`_ to your local account. +(`How to fork a repo `_). + + +Next, clone your fork to your computer so that you have a local copy. We recommend setting up a +directory in your home directory to put your version-controlled repositories (e.g. called :code:`git`). +There are two ways you can clone a repository: + +1. From a terminal (checkout: https://docs.github.com/en/get-started/quickstart/set-up-git for an tutorial) :: + + git clone https://github.com/YOUR-USERNAME/SimPEG + +2. Using a desktop client such as SourceTree_ or GitKraken_. + + .. image:: ../../../images/sourceTreeSimPEG.png + :align: center + :width: 400 + :target: https://www.sourcetreeapp.com/ + + If this is your first time managing a github_ repository through SourceTree_, + it is also handy to set up the remote account so it remembers your github_ + user name and password + + .. image:: ../../../images/sourceTreeRemote.png + :align: center + :width: 400 + +For managing your copy of SimPEG and contributing back to the main +repository, have a look at the article: `A successful git branching model +`_ + +.. _Github: https://github.com +.. _SourceTree: https://www.sourcetreeapp.com/ +.. _GitKraken: https://www.gitkraken.com/ + + + diff --git a/docs/content/basic/installing.rst b/docs/content/basic/installing.rst index 0044b4d981..61eb1866da 100644 --- a/docs/content/basic/installing.rst +++ b/docs/content/basic/installing.rst @@ -9,16 +9,28 @@ Getting Started with SimPEG Prerequisite: Installing Python =============================== -We highly recommend installing python using -`Anaconda `_ (or the alternative -`Mambaforge `_). -It installs `python `_, +SimPEG is written in Python_! +We highly recommend installing it using Anaconda_ (or the alternative Mambaforge_). +It installs `Python `_, `Jupyter `_ and other core -python libraries for scientific computing. +Python libraries for scientific computing. +If you and Python_ are not yet acquainted, we highly +recommend checking out `Software Carpentry `_. -As of version 0.11.0, we will no longer ensure compatibility with Python 2.7. Please use -the latest version of Python 3 with SimPEG. For more information on the transition of the -Python ecosystem to Python 3, please see the `Python 3 Statement `_. +.. note:: + + As of version 0.11.0, we will no longer ensure compatibility with Python 2.7. Please use + the latest version of Python 3 with SimPEG. For more information on the transition of the + Python ecosystem to Python 3, please see the `Python 3 Statement `_. + +.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png + :align: right + :width: 100 + :target: https://www.python.org/ + +.. _Python: https://www.python.org/ +.. _Anaconda: https://www.anaconda.com/products/individual +.. _Mambaforge: https://www.anaconda.com/products/individual .. _installing_simpeg: diff --git a/docs/content/basic/installing_for_developers.rst b/docs/content/basic/installing_for_developers.rst deleted file mode 100644 index 25c0ea1bf2..0000000000 --- a/docs/content/basic/installing_for_developers.rst +++ /dev/null @@ -1,180 +0,0 @@ -.. _getting_started_developers: - -Getting Started: for Developers -=============================== - -- **Purpose:** To download and set up your environment for using and developing within SimPEG. - - -.. _getting_started_installing_python: - -Installing Python ------------------ - -.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png - :align: right - :width: 100 - :target: https://www.python.org/ - -SimPEG is written in Python_! To install and maintain your Python_ -environment, Anaconda_ is a package manager that you can use. -If you and Python_ are not yet acquainted, we highly -recommend checking out `Software Carpentry `_. - -.. _Python: https://www.python.org/ - -.. _Anaconda: https://www.anaconda.com/products/individual - -.. _getting_started_working_with_git_and_github: - -Working with Git and GitHub ---------------------------- - -.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png - :align: right - :width: 100 - :target: https://github.com - - -To keep track of your code changes and contribute back to SimPEG, you will -need a github_ account, then fork the `SimPEG repository `_ -to your local account. -(`How to fork a repo `_). - - -.. _github: https://github.com - -Next, clone your fork to your computer so that you have a local copy. We recommend setting up a -directory in your home directory to put your version-controlled repositories (e.g. called :code:`git`). -There are two ways you can clone a repository: - -1. From a terminal (checkout: https://docs.github.com/en/get-started/quickstart/set-up-git for an tutorial) :: - - git clone https://github.com/YOUR-USERNAME/SimPEG - -.. _SourceTree: https://www.sourcetreeapp.com/ - -.. _GitKraken: https://www.gitkraken.com/ - -2. Using a desktop client such as SourceTree_ or GitKraken_. - - .. image:: ../../images/sourceTreeSimPEG.png - :align: center - :width: 400 - :target: https://www.sourcetreeapp.com/ - - If this is your first time managing a github_ repository through SourceTree_, - it is also handy to set up the remote account so it remembers your github_ - user name and password - - .. image:: ../../images/sourceTreeRemote.png - :align: center - :width: 400 - -For managing your copy of SimPEG and contributing back to the main -repository, have a look at the article: `A successful git branching model -`_ - - -.. _getting_started_setting_up_your_environment: - -Setting up your environment ---------------------------- - -To get started developing SimPEG we recommend setting up an environment using the ``conda``( or ``mamba``) -package manager that mimics the testing environment used for continuous integration testing. Most of the -packages that we use are available through the ``conda-forge`` project. This will -ensure you have all of the necessary packages to both develop SimPEG and run tests -locally. We provide an ``environment_test.yml`` in the base level directory. :: - - conda env create -f environment_test.yml - -.. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). - -There are many options to install SimPEG into this local environment, we recommend -using `pip`. After ensuring that all necessary packages from `environment_test.yml` -are installed, the most robust command you can use, executed from the base level directory -would be :: - - pip install --no-deps -e . - -This is called an editable mode install (`-e`). This will make a symbolic link for you to -the working simpeg directory for that python environment to use and you can then -make use of any changes you have made to the repository without re-installing it. This -command (`--no-deps`) also ensures pip won't unintentionally re-install a package that -was previously installed with conda. This practice also allows you to uninstall SimPEG -if so desired :: - - pip uninstall SimPEG - -.. note:: - We no longer recommend modifying your python path environment variable as a way - to install SimPEG for developers. - -.. _getting_started_jupyter_notebook: - -Jupyter Notebook ----------------- - -.. image:: https://raw.githubusercontent.com/jupyter/design/master/logos/Square%20Logo/squarelogo-greytext-orangebody-greymoons/squarelogo-greytext-orangebody-greymoons.svg - :align: right - :width: 100 - -The SimPEG team loves the `Jupyter notebook`_. It is an interactive -development environment. It is installed it you used Anaconda_ and can be -launched from a terminal using:: - - jupyter notebook - - -.. _getting_started_if_all_is_well: - -If all is well ... ------------------- - -You should be able to open a terminal within SimPEG/tutorials and run an example, ie.:: - - python 02-linear_inversion/plot_inv_1_inversion_lsq.py - -or you can download and run the :ref:`notebook from the docs `. - -.. image:: /content/tutorials/02-linear_inversion/images/sphx_glr_plot_inv_1_inversion_lsq_003.png - -You are now set up to SimPEG! - -If all is not well ... ----------------------- - -Submit an issue_ and `change this file`_! - -.. _issue: https://github.com/simpeg/simpeg/issues - -.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/api_getting_started_developers.rst - - -Advanced: Installing Solvers ----------------------------- - -Pardiso_ is a direct solvers that can be used for solving large(ish) -linear systems of equations. The provided testing environment should install -the necessary solvers for you. pymatsolver_ If you wish to modify pymatsolver_ as well -follow the instructions to download and install pymatsolver_. - -.. _Pardiso: https://www.pardiso-project.org - -.. _pymatsolver: https://github.com/rowanc1/pymatsolver - -If you open a `Jupyter notebook`_ and are able to run:: - - from pymatsolver import Pardiso - -.. _Jupyter notebook: http://jupyter.org/ - -then you have succeeded! Otherwise, make an `issue in pymatsolver`_. - -.. _issue in pymatsolver: https://github.com/rowanc1/pymatsolver/issues diff --git a/docs/content/basic/practices.rst b/docs/content/basic/practices.rst deleted file mode 100644 index eff9542b39..0000000000 --- a/docs/content/basic/practices.rst +++ /dev/null @@ -1,267 +0,0 @@ -.. _practices: - -Practices -========= - -- **Purpose**: In the development of SimPEG, we strive to follow best practices. Here, we - provide an overview of those practices and some tools we use to support them. - -Here we cover - -- testing_ -- style_ -- licensing_ - -.. _testing: - -Testing -------- - -.. image:: https://dev.azure.com/simpeg/simpeg/_apis/build/status/simpeg.simpeg?branchName=main - :target: https://dev.azure.com/simpeg/simpeg/_build/latest?definitionId=2&branchName=main - :alt: Azure pipeline - -.. image:: https://codecov.io/gh/simpeg/simpeg/branch/main/graph/badge.svg - :target: https://codecov.io/gh/simpeg/simpeg - :alt: Coverage status - -On each update, SimPEG is tested using the continuous integration service -`Azure pipelines `_. -We use `Codecov `_ to check and provide stats on how much -of the code base is covered by tests. This tells which lines of code have been -run in the test suite. It does not tell you about the quality of the tests run! -In order to assess that, have a look at the tests we are running - they tell you -the assumptions that we do not want to break within the code base. - -Within the repository, the tests are located in the top-level **tests** -directory. Tests are organized similar to the structure of the repository. -There are several types of tests we employ, this is not an exhaustive list, -but meant to provide a few places to look when you are developing and would -like to check that the code you wrote satisfies the assumptions you think it -should. - -Testing is performed with :code:`pytest` which is available through PyPI. -Checkout the docs on `pytest `_. - - -Compare with known values -^^^^^^^^^^^^^^^^^^^^^^^^^ - -In a simple case, you might know the exact value of what the output should be -and you can :code:`assert` that this is in fact the case. For example, -we setup a 3D :code:`BaseRectangularMesh` and assert that it has 3 dimensions. - -.. code:: python - - from discretize.base import BaseRectangularMesh - import numpy as np - - mesh = BaseRectangularMesh([6, 2, 3]) - - def test_mesh_dimensions(): - assert mesh.dim == 3 - -All functions with the naming convention :code:`test_XXX` -are run. Here we check that the dimensions are correct for the 3D mesh. - -If the value is not an integer, you can be subject to floating point errors, -so :code:`assert ==` might be too harsh. In this case, you will want to use -the ``numpy.testing`` module to check for approximate equals. For instance, - -.. code:: python - - import numpy as np - import discretize - from SimPEG import maps - - def test_map_multiplication(self): - mesh = discretize.TensorMesh([2,3]) - exp_map = maps.ExpMap(mesh) - vert_map = maps.SurjectVertical1D(mesh) - combo = exp_map*vert_map - m = np.arange(3.0) - t_true = np.exp(np.r_[0,0,1,1,2,2.]) - np.testing.assert_allclose(combo * m, t_true) - -These are rather simple examples, more advanced tests might include `solving an -electromagnetic problem numerically and comparing it to an analytical -solution `_ , or -`performing an adjoint test `_ to test :code:`Jvec` and :code:`Jtvec`. - - -.. _order_test: - -Order and Derivative Tests -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Order tests can be used when you are testing differential operators (we are using a second-order, -staggered grid discretization for our operators). For example, testing a 2D -curl operator in `test_operators.py `_ - -.. code:: python - - import numpy as np - import unittest - from discretize.tests import OrderTest - - class TestCurl2D(OrderTest): - name = "Cell Grad 2D - Dirichlet" - meshTypes = ['uniformTensorMesh'] - meshDimension = 2 - meshSizes = [8, 16, 32, 64] - - def getError(self): - # Test function - ex = lambda x, y: np.cos(y) - ey = lambda x, y: np.cos(x) - sol = lambda x, y: -np.sin(x)+np.sin(y) - - sol_curl2d = call2(sol, self.M.gridCC) - Ec = cartE2(self.M, ex, ey) - sol_ana = self.M.edge_curl*self.M.project_face_vector(Ec) - err = np.linalg.norm((sol_curl2d-sol_ana), np.inf) - - return err - - def test_order(self): - self.orderTest() - -Derivative tests are a particular type or :ref:`order_test`, and since they -are used so extensively, SimPEG includes a :code:`check_derivative` method. - -In the case -of testing a derivative, we consider a Taylor expansion of a function about -:math:`x`. For a small perturbation :math:`\Delta x`, - -.. math:: - - f(x + \Delta x) \simeq f(x) + J(x) \Delta x + \mathcal{O}(h^2) - -As :math:`\Delta x` decreases, we expect :math:`\|f(x) - f(x + \Delta x)\|` to -have first order convergence (e.g. the improvement in the approximation is -directly related to how small :math:`\Delta x` is, while if we include the -first derivative in our approximation, we expect that :math:`\|f(x) + -J(x)\Delta x - f(x + \Delta x)\|` to converge at a second-order rate. For -example, all `maps have an associated derivative test `_ . An example from `test_FDEM_derivs.py `_ - -.. code:: python - - def deriv_test(fdemType, comp): - - # setup simulation, survey - - def fun(x): - return survey.dpred(x), lambda x: sim.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) - -.. _documentation: - -Documentation -------------- - -Documentation helps others use your code! Please document new contributions. -SimPEG tries to follow the `numpydoc` style of docstrings (check out the -`style guide `_). -SimPEG then uses `sphinx `_ to build the documentation. -When documenting a new class or function, please include a description -(with math if it solves an equation), inputs, outputs and preferably a small example. - -For example: - -.. code:: python - - - class WeightedLeastSquares(BaseComboRegularization): - r"""Weighted least squares measure on model smallness and smoothness. - - L2 regularization with both smallness and smoothness (first order - derivative) contributions. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh on which the model parameters are defined. This is used - for constructing difference operators for the smoothness terms. - active_cells : array_like of bool or int, optional - List of active cell indices, or a `mesh.n_cells` boolean array - describing active cells. - alpha_s : float, optional - Smallness weight - alpha_x, alpha_y, alpha_z : float or None, optional - First order smoothness weights for the respective dimensions. - `None` implies setting these weights using the `length_scale` - parameters. - alpha_xx, alpha_yy, alpha_zz : float, optional - Second order smoothness weights for the respective dimensions. - length_scale_x, length_scale_y, length_scale_z : float, optional - First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional - A mapping to apply to the model before regularization. - reference_model : array_like, optional - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict or array_like, optional - User defined weights. It is recommended to interact with weights using - the `get_weights`, `set_weights` functionality. - - Notes - ----- - The function defined here approximates: - - .. math:: - \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - - Note if the key word argument `reference_model_in_smooth` is False, then mref is not - included in the smoothness contribution. - - If length scales are used to set the smoothness weights, alphas are respectively set internally using: - >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 - """ - - -.. _style: - -Style ------ - -Consistency makes code more readable and easier for collaborators to jump in. -`PEP 8 `_ provides conventions for -coding in Python. SimPEG is currently not `PEP 8 -`_ compliant, but we are working -towards it and would appreciate contributions that do too! Often, most python -text editors can be configured to issue warnings for non-compliant styles. - -SimPEG uses `black `_ version 23.1.0 to autoformat -the code base, and all additions to the code are tested to ensure that they are -compliant with `black`. We recommend installing `pre-commit `_ -hooks that are run on every commit to automatically ensure compliance. - -We also actively update the code base to ensure pep8 compliance by checking with -`flake8 `_ This performs style checks that could lead -towards bugs, performs checks on consistent documentation formatting, or just -identify poor coding practices. This is an ongoing process where we are fixing one -style warning at a time. The fixed style warnings are checked to ensure no new code -goes against an already established style. This test can also be installed locally -using pre-commit hooks, similar to `black` above. - - -.. _licensing: - -Licensing ---------- - -.. image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://github.com/simpeg/simpeg/blob/main/LICENSE - :alt: MIT license - -We want SimPEG to be a useful resource for the geoscience community and -believe that following open development practices is the best way to do that. -SimPEG is licensed under the `MIT license -`_ which is allows open -and commercial use and extension of SimPEG. It does not force packages that -use SimPEG to be open source nor does it restrict commercial use. diff --git a/docs/content/getting_started.rst b/docs/content/getting_started.rst index 907eaf8766..ecaa7745a0 100644 --- a/docs/content/getting_started.rst +++ b/docs/content/getting_started.rst @@ -11,6 +11,4 @@ Here you'll find instructions on getting up and running with ``SimPEG``. basic/big_picture basic/installing - basic/installing_for_developers - basic/practices - basic/contributing + basic/contributing/index.rst From 00afdeaaa40c3668603e23bdd2de06f2a28e7b38 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 16 Jun 2023 14:56:26 -0700 Subject: [PATCH 214/455] Show toctree in Contributing --- docs/content/basic/contributing/index.rst | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/content/basic/contributing/index.rst b/docs/content/basic/contributing/index.rst index f5b9c20ec5..6c49d078cc 100644 --- a/docs/content/basic/contributing/index.rst +++ b/docs/content/basic/contributing/index.rst @@ -70,16 +70,8 @@ fixes a bug, adds a new feature, improves the documentation or extends our tests. In the following pages you'll find information on how to get started to contribute with new code to SimPEG. -* :ref:`working-with-github` -* :ref:`setting-up-environment` -* :ref:`code-style` -* :ref:`documentation` -* :ref:`testing` -* :ref:`pull-requests` -* :ref:`advanced` - .. toctree:: - :hidden: + :maxdepth: 1 working-with-github setting-up-environment From b951592e5b286eee3df922a1c09c501fd2e92d85 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:18:36 -0700 Subject: [PATCH 215/455] Fix grammar on solvers Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/basic/contributing/advanced.rst index 2fcf5531b6..caaa369b0e 100644 --- a/docs/content/basic/contributing/advanced.rst +++ b/docs/content/basic/contributing/advanced.rst @@ -3,7 +3,7 @@ Advanced: Installing Solvers ---------------------------- -Pardiso_ is a direct solvers that can be used for solving large(ish) +Pardiso_ is a direct solver that can be used for solving large(ish) linear systems of equations. The provided testing environment should install the necessary solvers for you. pymatsolver_ If you wish to modify pymatsolver_ as well follow the instructions to download and install pymatsolver_. From e6a51c4e319012d5b72bf037b4bca34dd3010df8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:19:03 -0700 Subject: [PATCH 216/455] Remove unneeded mention of pymatsolver Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/basic/contributing/advanced.rst index caaa369b0e..a3eaf252b2 100644 --- a/docs/content/basic/contributing/advanced.rst +++ b/docs/content/basic/contributing/advanced.rst @@ -5,7 +5,7 @@ Advanced: Installing Solvers Pardiso_ is a direct solver that can be used for solving large(ish) linear systems of equations. The provided testing environment should install -the necessary solvers for you. pymatsolver_ If you wish to modify pymatsolver_ as well +the necessary solvers for you. If you wish to modify pymatsolver_ as well follow the instructions to download and install pymatsolver_. .. _Pardiso: https://www.pardiso-project.org From 582f0100c47ed7347e7edb9af064c19d0ace0a9b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:19:18 -0700 Subject: [PATCH 217/455] Use single colon at the end of paragraph Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/basic/contributing/advanced.rst index a3eaf252b2..e731de50fa 100644 --- a/docs/content/basic/contributing/advanced.rst +++ b/docs/content/basic/contributing/advanced.rst @@ -12,7 +12,7 @@ follow the instructions to download and install pymatsolver_. .. _pymatsolver: https://github.com/rowanc1/pymatsolver -If you open a `Jupyter notebook`_ and are able to run:: +If you open a `Jupyter notebook`_ and are able to run: from pymatsolver import Pardiso From 41977d0ca48b7fc22cc7624ded03a2f878dca222 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:19:43 -0700 Subject: [PATCH 218/455] Use rst directive `code` instead of double colon Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/advanced.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/basic/contributing/advanced.rst index e731de50fa..eb4a01bf90 100644 --- a/docs/content/basic/contributing/advanced.rst +++ b/docs/content/basic/contributing/advanced.rst @@ -14,6 +14,8 @@ follow the instructions to download and install pymatsolver_. If you open a `Jupyter notebook`_ and are able to run: +.. code:: python + from pymatsolver import Pardiso .. _Jupyter notebook: http://jupyter.org/ From d78415047f4ea504d5740962ccaf2c7c97d722af Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:19:57 -0700 Subject: [PATCH 219/455] Fix grammar in identify Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/code-style.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/code-style.rst b/docs/content/basic/contributing/code-style.rst index 21ff034fc0..2354b94d00 100644 --- a/docs/content/basic/contributing/code-style.rst +++ b/docs/content/basic/contributing/code-style.rst @@ -8,7 +8,7 @@ SimPEG uses Black_ to autoformat its codebase, and flake8_ to lint its code and enforce style rules. Black_ can automatically format SimPEG's codebase to ensure it complies with Black code style. flake8_ performs style checks, raises warnings on code that could lead towards bugs, performs checks on consistent -documentation formatting, and identify poor coding practices. +documentation formatting, and identifies poor coding practices. .. important:: From 191bc2594a4094bdba475656722356cea44942f2 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:20:26 -0700 Subject: [PATCH 220/455] Fix grammar in "these" Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/index.rst b/docs/content/basic/contributing/index.rst index 6c49d078cc..afc6da246b 100644 --- a/docs/content/basic/contributing/index.rst +++ b/docs/content/basic/contributing/index.rst @@ -6,7 +6,7 @@ Contributing to SimPEG First of all, we are glad you are here! We welcome contributions and input from the community. -In this pages we set some guidelines for contributing to the repositories +In these pages we set some guidelines for contributing to the repositories hosted in the `SimPEG `_ organization on GitHub. These repositories are maintained on a volunteer basis. From 5af92148d74d601a0786d19fa5a4b697cd7854ff Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:20:49 -0700 Subject: [PATCH 221/455] Add "as possible" Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/index.rst b/docs/content/basic/contributing/index.rst index afc6da246b..b0c1ef4a3e 100644 --- a/docs/content/basic/contributing/index.rst +++ b/docs/content/basic/contributing/index.rst @@ -40,7 +40,7 @@ Bugs If you found a bug in SimPEG, please report it through a `new Issue `_. -Please be as descriptive and provide sufficient detail to reproduce the error. +Please be as descriptive as possible and provide sufficient detail to reproduce the error. Whenever possible, if you can include a small example that produces the error, this will help us resolve issues faster. From 40f37902aa3c2f2e979b60369573dafd1a315cb7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:21:04 -0700 Subject: [PATCH 222/455] Replace "produces" for "reproduces" Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/index.rst b/docs/content/basic/contributing/index.rst index b0c1ef4a3e..66a10fd678 100644 --- a/docs/content/basic/contributing/index.rst +++ b/docs/content/basic/contributing/index.rst @@ -41,7 +41,7 @@ Bugs If you found a bug in SimPEG, please report it through a `new Issue `_. Please be as descriptive as possible and provide sufficient detail to reproduce the error. -Whenever possible, if you can include a small example that produces the error, +Whenever possible, if you can include a small example that reproduces the error, this will help us resolve issues faster. .. _suggest-enhancements: From 761b0dd0e5e60fe55424cdbd5ae36d5d5f8486b7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:21:30 -0700 Subject: [PATCH 223/455] Use rst directive `code` Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/setting-up-environment.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst index 7f38cf6b3a..efa951b889 100644 --- a/docs/content/basic/contributing/setting-up-environment.rst +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -46,6 +46,8 @@ recommend using `pip`. After ensuring that all necessary packages from `environment_test.yml` are installed, the most robust command you can use, executed from the base level directory would be :: +.. code:: + pip install --no-deps -e . This is called an editable mode install (`-e`). This will make a symbolic link From 8502830b44db50707e7200a30b9200617812bf0d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:22:08 -0700 Subject: [PATCH 224/455] Remove double colon --- docs/content/basic/contributing/setting-up-environment.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst index efa951b889..cf677114e4 100644 --- a/docs/content/basic/contributing/setting-up-environment.rst +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -44,7 +44,7 @@ Install SimPEG in developer mode There are many options to install SimPEG into this local environment, we recommend using `pip`. After ensuring that all necessary packages from `environment_test.yml` are installed, the most robust command you can use, -executed from the base level directory would be :: +executed from the base level directory would be: .. code:: From 3c12b9cac741c924e1c6539fd691e647d39862eb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:23:22 -0700 Subject: [PATCH 225/455] Use rst `code` directive instead of double colon Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/setting-up-environment.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst index cf677114e4..2515c9b5da 100644 --- a/docs/content/basic/contributing/setting-up-environment.rst +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -55,7 +55,9 @@ for you to the working ``simpeg`` directory for that Python environment to use and you can then make use of any changes you have made to the repository without re-installing it. This command (`--no-deps`) also ensures pip won't unintentionally re-install a package that was previously installed with conda. -This practice also allows you to uninstall SimPEG if so desired :: +This practice also allows you to uninstall SimPEG if so desired: + +.. code:: pip uninstall SimPEG From 3e977b816678bf9494109d07f8dfa54b37a6db66 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:23:42 -0700 Subject: [PATCH 226/455] Replace "or" for "of" Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/testing.rst b/docs/content/basic/contributing/testing.rst index 0d5e6abefb..1a5660018b 100644 --- a/docs/content/basic/contributing/testing.rst +++ b/docs/content/basic/contributing/testing.rst @@ -115,7 +115,7 @@ example, testing a 2D curl operator in `test_operators.py def test_order(self): self.orderTest() -Derivative tests are a particular type or :ref:`order_test`, and since they +Derivative tests are a particular type of :ref:`order_test`, and since they are used so extensively, SimPEG includes a :code:`check_derivative` method. In the case From e7f97502b24f18b1cc982fff5b743cc40c03df72 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:24:35 -0700 Subject: [PATCH 227/455] Replace "SimPEG" for "discretize" Co-authored-by: Joseph Capriotti --- docs/content/basic/contributing/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/basic/contributing/testing.rst b/docs/content/basic/contributing/testing.rst index 1a5660018b..ba8de39983 100644 --- a/docs/content/basic/contributing/testing.rst +++ b/docs/content/basic/contributing/testing.rst @@ -116,7 +116,7 @@ example, testing a 2D curl operator in `test_operators.py self.orderTest() Derivative tests are a particular type of :ref:`order_test`, and since they -are used so extensively, SimPEG includes a :code:`check_derivative` method. +are used so extensively, discretize includes a :code:`check_derivative` method. In the case of testing a derivative, we consider a Taylor expansion of a function about From 927eefdbe6335a9da522047b040be195af1d6989 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 14:56:20 -0700 Subject: [PATCH 228/455] Minor improvements to setting up your environment --- .../basic/contributing/setting-up-environment.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst index 2515c9b5da..42b830310c 100644 --- a/docs/content/basic/contributing/setting-up-environment.rst +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -14,17 +14,22 @@ Create environment ------------------ To get started developing SimPEG we recommend setting up an environment using -the ``conda`` ( or ``mamba``) package manager that mimics the testing +the ``conda`` (or ``mamba``) package manager that mimics the testing environment used for continuous integration testing. Most of the packages that we use are available through the ``conda-forge`` project. This will ensure you have all of the necessary packages to both develop SimPEG and run tests locally. We provide an ``environment_test.yml`` in the base level directory. +To create the environment and install all packages needed to run and write code +for SimPEG, navigate to the directory where you :ref:`cloned SimPEG's +repository ` and run: + .. code:: conda env create -f environment_test.yml .. note:: + If you find yourself wanting a faster package manager than ``conda`` check out the ``mamba`` project at https://mamba.readthedocs.io/. It usually is able to set up environments much quicker than ``conda`` and @@ -73,7 +78,7 @@ Check your installation ----------------------- You should be able to open a terminal within SimPEG/tutorials and run an -example, ie. +example, i.e. .. code:: From 3b2d43960fd6f3a10856b3cc3ed048436d3a68af Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 15:04:51 -0700 Subject: [PATCH 229/455] Remove unused import --- SimPEG/objective_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 3f568f6b58..c7a41b357c 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -5,7 +5,7 @@ from .maps import IdentityMap from .props import BaseSimPEG -from .utils import set_kwargs, timeIt, Zero, Identity +from .utils import timeIt, Zero, Identity __all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] From 1763c995fcf4c9271b788028b168eb1059b7258e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 16:17:48 -0700 Subject: [PATCH 230/455] Remove kwargs from L2ObjectiveFunction --- SimPEG/objective_function.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index c7a41b357c..fa0c6d3f08 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -473,7 +473,6 @@ def __init__( has_fields=False, counter=None, debug=False, - **kwargs, ): # Check if nP and shape of W are consistent if W is not None and nP is not None and nP != W.shape[1]: @@ -487,7 +486,6 @@ def __init__( has_fields=has_fields, debug=debug, counter=counter, - **kwargs, ) if W is not None and self.nP == "*": self._nP = W.shape[1] From f2b477d35639b9781718444410943537ea24939b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 16:30:14 -0700 Subject: [PATCH 231/455] Test error on indActive and active_cells Test if error is raised after simultaneously passing `active_cells` and `indActive` to `BaseRegularization` and `WeightedLeastSquares`. --- tests/base/test_regularization.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 497d486780..c6db65a65d 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -627,5 +627,44 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) +class TestIndActiveAndActiveCells: + """Test error after simultaneously passing indActive and active_cells.""" + + @pytest.fixture(params=["1D", "2D", "3D"]) + def mesh(self, request): + """Sample mesh.""" + if request.param == "1D": + hx = np.random.rand(10) + hx = hx / hx.sum() + h = [hx] + elif request.param == "2D": + hx, hy = np.random.rand(10), np.random.rand(9) + hx, hy = hx / hx.sum(), hy / hy.sum() + h = [hx, hy] + elif request.param == "3D": + hx, hy, hz = np.random.rand(10), np.random.rand(9), np.random.rand(8) + hx, hy, hz = hx / hx.sum(), hy / hy.sum(), hz / hz.sum() + h = [hx, hy, hz] + return discretize.TensorMesh(h) + + def test_base_regularization(self, mesh): + """Test BaseRegularization.""" + active_cells = np.ones(len(mesh), dtype=bool) + msg = "Cannot simultanously pass 'active_cells' and 'indActive'." + with pytest.raises(ValueError, match=msg): + regularization.BaseRegularization( + mesh, active_cells=active_cells, indActive=active_cells + ) + + def test_weighted_least_squares(self, mesh): + """Test WeightedLeastSquares.""" + active_cells = np.ones(len(mesh), dtype=bool) + msg = "Cannot simultanously pass 'active_cells' and 'indActive'." + with pytest.raises(ValueError, match=msg): + regularization.WeightedLeastSquares( + mesh, active_cells=active_cells, indActive=active_cells + ) + + if __name__ == "__main__": unittest.main() From 81d9ca77b956d53d57f8eb9ffe44fddbbea428bc Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 16:32:28 -0700 Subject: [PATCH 232/455] Remove repeated attributes from BaseDataMisfit --- SimPEG/data_misfit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py index d1ecb6e705..c111e42555 100644 --- a/SimPEG/data_misfit.py +++ b/SimPEG/data_misfit.py @@ -21,8 +21,6 @@ def __init__(self, data, simulation, debug=False, counter=None, **kwargs): self.data = data self.simulation = simulation - self.debug = debug - self.count = counter @property def data(self): From a47a4075ecac14827c30202fca2bc783bea779d1 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 19 Jun 2023 16:35:29 -0700 Subject: [PATCH 233/455] Simplify fixture --- tests/base/test_regularization.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index c6db65a65d..87c4c8d223 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -635,16 +635,13 @@ def mesh(self, request): """Sample mesh.""" if request.param == "1D": hx = np.random.rand(10) - hx = hx / hx.sum() - h = [hx] + h = [hx / hx.sum()] elif request.param == "2D": hx, hy = np.random.rand(10), np.random.rand(9) - hx, hy = hx / hx.sum(), hy / hy.sum() - h = [hx, hy] + h = [h_i / h_i.sum() for h_i in (hx, hy)] elif request.param == "3D": hx, hy, hz = np.random.rand(10), np.random.rand(9), np.random.rand(8) - hx, hy, hz = hx / hx.sum(), hy / hy.sum(), hz / hz.sum() - h = [hx, hy, hz] + h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] return discretize.TensorMesh(h) def test_base_regularization(self, mesh): From fb1d35ae6e5b5babb7bae5a534c887adb1f1b5e4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:10:01 -0700 Subject: [PATCH 234/455] Replace indentation tabs for spaces in .flake8 --- .flake8 | 100 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/.flake8 b/.flake8 index b0d4d63536..af1b4b7dd8 100644 --- a/.flake8 +++ b/.flake8 @@ -34,107 +34,107 @@ exclude-from-doctest = # solved througout the entire project, it should be removed to this list. ignore = # assertRaises(Exception): should be considered evil - B017, + B017, # No explicit stacklevel argument found. - B028, + B028, # Missing docstring in public module - D100, + D100, # Missing docstring in public class - D101, + D101, # Missing docstring in public method - D102, + D102, # Missing docstring in public function - D103, + D103, # Missing docstring in public package - D104, + D104, # Missing docstring in magic method - D105, + D105, # Missing docstring in __init__ - D107, + D107, # One-line docstring should fit on one line with quotes - D200, + D200, # No blank lines allowed before function docstring - D201, + D201, # No blank lines allowed after function docstring - D202, + D202, # 1 blank line required between summary line and description - D205, + D205, # Docstring is over-indented - D208, + D208, # Multi-line docstring closing quotes should be on a separate line - D209, + D209, # No whitespaces allowed surrounding docstring text - D210, + D210, # No blank lines allowed before class docstring - D211, + D211, # Use """triple double quotes""" - D300, + D300, # First line should end with a period - D400, + D400, # First line should be in imperative mood; try rephrasing D401, # First line should not be the function's "signature" - D402, + D402, # First word of the first line should be properly capitalized - D403, + D403, # No blank lines allowed between a section header and its content - D412, + D412, # Section has no content - D414, + D414, # Docstring is empty - D419, + D419, # module level import not at top of file - E402, + E402, # comparison to None should be ‘if cond is None:’ - E711, + E711, # do not assign a lambda expression, use a def - E731, + E731, # 'from %s import *' used; unable to detect undefined names - F403, + F403, # %r may be undefined, or defined from star imports: %s - F405, + F405, # '...'.format(...) has unused named argument(s): %s - F522, + F522, # '...'.format(...) has unused arguments at position(s): %s - F523, + F523, # '...'.format(...) is missing argument(s) for placeholder(s): %s - F524, + F524, # f-string is missing placeholders - F541, + F541, # redefinition of unused %r from line %r - F811, + F811, # undefined name %r - F821, + F821, # Block quote ends without a blank line; unexpected unindent. - RST201, + RST201, # Definition list ends without a blank line; unexpected unindent. - RST203, + RST203, # Field list ends without a blank line; unexpected unindent. - RST206, + RST206, # Inline strong start-string without end-string. - RST210, + RST210, # Title underline too short. - RST212, + RST212, # Inline emphasis start-string without end-string. - RST213, + RST213, # Inline interpreted text or phrase reference start-string without end-string. - RST215, + RST215, # Inline substitution_reference start-string without end-string. - RST219, + RST219, # Unexpected indentation. - RST301, + RST301, # Unknown directive type "*". - RST303, + RST303, # Unknown interpreted text role "*". - RST304, + RST304, # Error in "*" directive: - RST307, + RST307, # Previously unseen severe error, not yet assigned a unique code. - RST499, + RST499, # trailing whitespace - W291, + W291, # blank line contains whitespace - W293, + W293, # Configure flake8-rst-docstrings From 6d0f91910b883afa1792e90b1595c427f7f0714e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:10:56 -0700 Subject: [PATCH 235/455] Ignore docs/_build when running flake8 --- .flake8 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index af1b4b7dd8..8bb414e57c 100644 --- a/.flake8 +++ b/.flake8 @@ -22,8 +22,9 @@ exclude = .git, __pycache__, .ipynb_checkpoints, - setup.py - docs/conf.py + setup.py, + docs/conf.py, + docs/_build/, per-file-ignores = # disable unused-imports errors on __init__.py __init__.py: F401 From 0cb3aa587a98a91727ec3fabec3483dcc50f1b71 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:18:03 -0700 Subject: [PATCH 236/455] Add space in help --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f29f7cbd3c..c4981ef69f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: @echo " check run code style and quality checks (black and flake8)" @echo " black checks code style with black" @echo " flake checks code style with flake8" - @echo " flake-all checks code style with flake8 (full set of rules)" + @echo " flake-all checks code style with flake8 (full set of rules)" @echo "" build: From 4f1c59c4ffe7ff34eaca158cb94e72dbaf6a276c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:19:28 -0700 Subject: [PATCH 237/455] Fix flake E711 error: wrong comparison with None Remove `E711` from the list of ignored errors in `Makefile`. Compare variables against `None` with `is` or `is not` instead of `==` and `!=`. --- Makefile | 1 - SimPEG/electromagnetics/static/utils/static_utils.py | 2 +- SimPEG/utils/io_utils/io_utils_electromagnetics.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 958d76e4a6..824fa87460 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ D414,\ D419,\ E402,\ - E711,\ E731,\ F403,\ F405,\ diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/SimPEG/electromagnetics/static/utils/static_utils.py index a4d746e041..b522e21194 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/SimPEG/electromagnetics/static/utils/static_utils.py @@ -852,7 +852,7 @@ def plot_3d_pseudosection( marker = {key: marker_opts.get(key, marker[key]) for key in marker} # 3D scatter plot - if plane_points == None: + if plane_points is None: marker["color"] = plot_vec scatter_data = [ grapho.Scatter3d( diff --git a/SimPEG/utils/io_utils/io_utils_electromagnetics.py b/SimPEG/utils/io_utils/io_utils_electromagnetics.py index 783c1abf36..25d833535d 100644 --- a/SimPEG/utils/io_utils/io_utils_electromagnetics.py +++ b/SimPEG/utils/io_utils/io_utils_electromagnetics.py @@ -1031,7 +1031,7 @@ def write_dcip_xyz( out_columns = np.c_[out_columns, data_object.standard_deviation] # Append additional columns from dictionary - if out_dict != None: + if out_dict is not None: for k in list(out_dict.keys()): out_headers += " " + k out_columns = np.c_[out_columns, out_dict[k]] From 86bdde5ed4b1da538d377b67927e01034234ce69 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:34:49 -0700 Subject: [PATCH 238/455] Fix flake `E731` error: assign lambda functions Remove `E731` from ignored flake rules in `Makefile`. Avoid assigning lambda functions and define proper functions for them. --- Makefile | 1 - .../analytics/FDEMDipolarfields.py | 6 ++- SimPEG/electromagnetics/analytics/NSEM.py | 39 +++++++++---------- .../electromagnetics/utils/testing_utils.py | 3 +- .../potential_fields/magnetics/analytics.py | 6 +-- tests/em/em1d/test_EM1D_FD_jac_layers.py | 27 ++++++++++--- .../em1d/test_EM1D_TD_general_jac_layers.py | 18 +++++++-- tests/em/em1d/test_EM1D_TD_off_jac_layers.py | 18 +++++++-- 8 files changed, 78 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index 958d76e4a6..656e1a13bd 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ D419,\ E402,\ E711,\ - E731,\ F403,\ F405,\ F522,\ diff --git a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py b/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py index 2b6247df55..9eebd4fd0f 100644 --- a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py +++ b/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py @@ -2,7 +2,11 @@ from scipy.constants import epsilon_0, mu_0 from SimPEG import utils -omega = lambda f: 2.0 * np.pi * f + +def omega(f): + return 2.0 * np.pi * f + + # TODO: # r = lambda dx, dy, dz: np.sqrt( dx**2. + dy**2. + dz**2.) # k = lambda f, mu, epsilon, sig: np.sqrt( omega(f)**2. *mu*epsilon -1j*omega(f)*mu*sig ) diff --git a/SimPEG/electromagnetics/analytics/NSEM.py b/SimPEG/electromagnetics/analytics/NSEM.py index 15a6aec0a4..b9c3dffc88 100644 --- a/SimPEG/electromagnetics/analytics/NSEM.py +++ b/SimPEG/electromagnetics/analytics/NSEM.py @@ -7,32 +7,31 @@ # Evaluate Impedance Z of a layer -_ImpZ = lambda f, mu, k: omega(f) * mu / k +def _ImpZ(f, mu, k): + return omega(f) * mu / k + # Complex Cole-Cole Conductivity - EM utils -_PCC = lambda siginf, m, t, c, f: siginf * ( - 1.0 - (m / (1.0 + (1j * omega(f) * t) ** c)) -) +def _PCC(siginf, m, t, c, f): + return siginf * (1.0 - (m / (1.0 + (1j * omega(f) * t) ** c))) + # matrix P relating Up and Down components with E and H fields -_P = lambda z: np.array( - [ - [ - 1.0, - 1, - ], - [-1.0 / z, 1.0 / z], - ] -) -_Pinv = lambda z: np.array([[1.0, -z], [1.0, z]]) / 2.0 +def _P(z): + return np.array([[1.0, 1.0], [-1.0 / z, 1.0 / z]]) + + +def _Pinv(z): + return np.array([[1.0, -z], [1.0, z]]) / 2.0 + # matrix T for transition of Up and Down components accross a layer -_T = lambda h, k: np.array( - [[np.exp(1j * k * h), 0.0], [0.0, np.exp(-1j * k * h)]], -) -_Tinv = lambda h, k: np.array( - [[np.exp(-1j * k * h), 0.0], [0.0, np.exp(1j * k * h)]], -) +def _T(h, k): + return np.array([[np.exp(1j * k * h), 0.0], [0.0, np.exp(-1j * k * h)]]) + + +def _Tinv(h, k): + return np.array([[np.exp(-1j * k * h), 0.0], [0.0, np.exp(1j * k * h)]]) # Propagate Up and Down component for a certain frequency & evaluate E and H field diff --git a/SimPEG/electromagnetics/utils/testing_utils.py b/SimPEG/electromagnetics/utils/testing_utils.py index bf8e774d5a..3315c71061 100644 --- a/SimPEG/electromagnetics/utils/testing_utils.py +++ b/SimPEG/electromagnetics/utils/testing_utils.py @@ -148,7 +148,8 @@ def crossCheckTest( TOL=1e-5, verbose=False, ): - l2norm = lambda r: np.sqrt(r.dot(r)) + def l2norm(r): + return np.sqrt(r.dot(r)) prb1 = getFDEMProblem(fdemType1, comp, SrcList, freq, useMu, verbose) mesh = prb1.mesh diff --git a/SimPEG/potential_fields/magnetics/analytics.py b/SimPEG/potential_fields/magnetics/analytics.py index 65f8a08333..a9bdd7ad1f 100644 --- a/SimPEG/potential_fields/magnetics/analytics.py +++ b/SimPEG/potential_fields/magnetics/analytics.py @@ -120,9 +120,9 @@ def CongruousMagBC(mesh, Bo, chi): indxd, indxu, indyd, indyu, indzd, indzu = mesh.face_boundary_indices const = mu_0 / (4 * np.pi) * mom - rfun = lambda x: np.sqrt( - (x[:, 0] - xc) ** 2 + (x[:, 1] - yc) ** 2 + (x[:, 2] - zc) ** 2 - ) + + def rfun(x): + return np.sqrt((x[:, 0] - xc) ** 2 + (x[:, 1] - yc) ** 2 + (x[:, 2] - zc) ** 2) mdotrx = ( mx diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index ee9221a0a6..83c78f9758 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -112,8 +112,11 @@ def jacfun(m, dm): Jvec = self.sim.Jvec(m, dm) return Jvec + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -156,7 +159,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -266,8 +271,11 @@ def jacfun(m, dm): Jvec = self.sim.Jvec(m, dm) return Jvec + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -310,7 +318,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -401,7 +411,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -441,7 +454,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index 78bff95ecb..dd90a32a72 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -80,7 +80,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -112,7 +115,9 @@ def misfit(m, dobs): dmisfit = sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) self.assertTrue(passed) @@ -255,7 +260,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -286,7 +294,9 @@ def misfit(m, dobs): dmisfit = sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) self.assertTrue(passed) diff --git a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py index 56d4a02b60..4345ffb426 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -111,7 +111,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -154,7 +157,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -266,7 +271,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -309,7 +317,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: From 356b153ac3f337851c2cb9474c82a8454c0ab424 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:54:04 -0700 Subject: [PATCH 239/455] Fix flake 403 error: don't use star imports Remove `403` from the list of ignored flake rules in `Makefile`. Replace star imports for explicit imports. --- Makefile | 1 - SimPEG/electromagnetics/analytics/__init__.py | 36 +++++++++++++++++-- .../static/resistivity/utils.py | 3 +- out | 8 +++++ 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 out diff --git a/Makefile b/Makefile index 958d76e4a6..0e8b7b03e2 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ E402,\ E711,\ E731,\ - F403,\ F405,\ F522,\ F523,\ diff --git a/SimPEG/electromagnetics/analytics/__init__.py b/SimPEG/electromagnetics/analytics/__init__.py index ebcc3a0946..7091e7fdca 100644 --- a/SimPEG/electromagnetics/analytics/__init__.py +++ b/SimPEG/electromagnetics/analytics/__init__.py @@ -1,6 +1,36 @@ from .TDEM import hzAnalyticDipoleT, hzAnalyticCentLoopT from .FDEM import hzAnalyticDipoleF -from .FDEMcasing import * -from .DC import * -from .FDEMDipolarfields import * +from .FDEMcasing import ( + getKc, + _r2, + _getCasingHertzMagDipole, + _getCasingHertzMagDipoleDeriv_r, + _getCasingHertzMagDipoleDeriv_z, + _getCasingHertzMagDipole2Deriv_z_r, + _getCasingHertzMagDipole2Deriv_z_z, + getCasingEphiMagDipole, + getCasingHrMagDipole, + getCasingHzMagDipole, + getCasingBrMagDipole, + getCasingBzMagDipole, +) +from .DC import ( + DCAnalytic_Pole_Dipole, + DCAnalytic_Dipole_Pole, + DCAnalytic_Pole_Pole, + DCAnalytic_Dipole_Dipole, + DCAnalyticSphere, + AnBnfun, +) +from .FDEMDipolarfields import ( + E_from_ElectricDipoleWholeSpace, + E_galvanic_from_ElectricDipoleWholeSpace, + E_inductive_from_ElectricDipoleWholeSpace, + J_from_ElectricDipoleWholeSpace, + J_galvanic_from_ElectricDipoleWholeSpace, + J_inductive_from_ElectricDipoleWholeSpace, + H_from_ElectricDipoleWholeSpace, + B_from_ElectricDipoleWholeSpace, + A_from_ElectricDipoleWholeSpace, +) from .NSEM import MT_LayeredEarth diff --git a/SimPEG/electromagnetics/static/resistivity/utils.py b/SimPEG/electromagnetics/static/resistivity/utils.py index e1974f5943..5722b1355c 100644 --- a/SimPEG/electromagnetics/static/resistivity/utils.py +++ b/SimPEG/electromagnetics/static/resistivity/utils.py @@ -1,11 +1,10 @@ import numpy as np +import matplotlib.pyplot as plt from . import receivers from . import sources from .survey import Survey -from ..utils import * - def WennerSrcList(n_electrodes, a_spacing, in2D=False, plotIt=False): """ diff --git a/out b/out new file mode 100644 index 0000000000..739071577c --- /dev/null +++ b/out @@ -0,0 +1,8 @@ +DCAnalytic_Pole_Dipole +DCAnalytic_Dipole_Pole +DCAnalytic_Pole_Pole +DCAnalytic_Dipole_Dipole +DCAnalyticSphere + + +AnBnfun From bbe14ba44075571324a8e746e6afb4eea221f001 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 14:57:44 -0700 Subject: [PATCH 240/455] Remove F405 from Makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 0e8b7b03e2..d63a07ca20 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ E402,\ E711,\ E731,\ - F405,\ F522,\ F523,\ F524,\ From 470c4d53bf49971a8e9d8c29cf4eb1609eb5b92e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:10:41 -0700 Subject: [PATCH 241/455] Fix `F522`, `F523`, `F524` flake errors: format calls Remove them from the ignored flake warning list in the Makefile. Solve issues regarding wrong calls to `format()`. Use f-strings instead. --- Makefile | 3 --- SimPEG/electromagnetics/static/resistivity/IODC.py | 5 ++--- tests/em/static/test_DC_Utils.py | 2 +- tests/em/static/test_DC_jvecjtvecadj.py | 5 ++--- tests/em/tdem/test_TDEM_DerivAdjoint.py | 6 +----- tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py | 6 +----- 6 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 958d76e4a6..bf347d948a 100644 --- a/Makefile +++ b/Makefile @@ -40,9 +40,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ E731,\ F403,\ F405,\ - F522,\ - F523,\ - F524,\ F541,\ F811,\ F821,\ diff --git a/SimPEG/electromagnetics/static/resistivity/IODC.py b/SimPEG/electromagnetics/static/resistivity/IODC.py index 5b06ddc5f1..5c7c6c3432 100644 --- a/SimPEG/electromagnetics/static/resistivity/IODC.py +++ b/SimPEG/electromagnetics/static/resistivity/IODC.py @@ -1253,9 +1253,8 @@ def read_ubc_dc2d_obs_file(self, filename, input_type="simple", toponame=None): topo = tmp_topo[1:, :] if topo.shape[0] != n_topo: print( - ">> # of points for the topography is not {0}, but {0}".format( - n_topo, topo.shape[0] - ) + ">> # of points for the topography is " + f"not {n_topo}, but {topo.shape[0]}" ) tmp = np.loadtxt(filename, comments="!").astype(float) e = np.zeros(tmp.shape[0], dtype=float) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 1b5edb914b..3d09ea1ecd 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -131,7 +131,7 @@ def test_io_rhoa(self): rhoA_GIF = np.loadtxt(rhoA_GIF_file) passed = np.allclose(rhoapp, rhoA_GIF) self.assertTrue(passed) - print(" ... ok \n".format(survey_type)) + print(f" ... ok ({survey_type})\n") def tearDown(self): # Clean up the working directory diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 70634d8c3f..057e16c36c 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -155,9 +155,8 @@ def test_e_adjoint(self): wJtv = w.dot(self.prob.Jtvec(m, v, u)) tol = np.max([TOL * (10 ** int(np.log10(np.abs(vJw)))), FLR]) print( - "vJw: {:1.2e}, wJTv: {:1.2e}, tol: {:1.0e}, passed: {}\n".format( - vJw, wJtv, vJw - wJtv, tol, np.abs(vJw - wJtv) < tol - ) + f"vJw: {vJw:1.2e}, wJTv: {wJtv:1.2e}, tol: {tol:1.0e}, " + f"passed: {np.abs(vJw - wJtv) < tol}\n" ) return np.abs(vJw - wJtv) < tol diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 5433ea9a4a..100af08490 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -133,11 +133,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print( - " {v1} {v2} {passed}".format( - prbtype=self.formulation, v1=V1, v2=V2, passed=passed - ) - ) + print(f"{prbtype} {V1} {V2} {passed}") self.assertTrue(passed) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 843fd2fa3b..f3daba41e8 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -150,11 +150,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print( - " {v1} {v2} {passed}".format( - prbtype=self.formulation, v1=V1, v2=V2, passed=passed - ) - ) + print(f"{prbtype} {V1} {V2} {passed}") self.assertTrue(passed) From 9d18d685750a149823290aa8f9dbe72b85415d2a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:17:42 -0700 Subject: [PATCH 242/455] Fix `F541` flake error: f-string without placeholder Remove `F541` from list of ignored flake warnings in `Makefile`. Avoid using f-strings without placeholders. --- Makefile | 1 - tests/base/test_regularization.py | 2 +- tests/em/nsem/inversion/test_complex_resistivity.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 958d76e4a6..a103777ce7 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,6 @@ FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ F522,\ F523,\ F524,\ - F541,\ F811,\ F821,\ RST201,\ diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 497d486780..5743482883 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -564,7 +564,7 @@ def test_vector_amplitude(self): mesh, maps.IdentityMap(nP=n_comp * mesh.nC) ) - with pytest.raises(ValueError, match=f"'weights' must be one of"): # noqa: W605 + with pytest.raises(ValueError, match="'weights' must be one of"): reg.set_weights(abc=(1.0, 1.0)) np.testing.assert_almost_equal( diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 209cac986a..07f42633bc 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -261,7 +261,7 @@ def check_deriv_adjoint(self, component, orientation): self.check_adjoint(sim3) self.check_deriv(sim4) self.check_adjoint(sim4) - print(f"... done") + print("... done") def test_apparent_resistivity_xx(self): self.check_deriv_adjoint("apparent_resistivity", "xx") From f0584946695da73590845bfbaaa5d11eb02ab66a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:24:52 -0700 Subject: [PATCH 243/455] Simplify CONTRIBUTING.md Move `CONTRIBUTING.rst` to `CONTRIBUTING.md` and simplify it by pointing to the documentation pages that contain the contributing guidelines. --- CONTRIBUTING.md | 16 ++++++ CONTRIBUTING.rst | 137 ----------------------------------------------- 2 files changed, 16 insertions(+), 137 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.rst diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..8545261c53 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing to SimPEG + +First of all, we are glad you are here! We welcome contributions and input +from the community. + +You can find guidelines on how to contribute to SimPEG in the [Contributing to +SimPEG](content/basic/contributing/index.html) section of our documentation. + + +## Licensing + +All code contributed to SimPEG is licensed under the [MIT license](LICENSE) +which allows open and commercial use and extension of SimPEG. If you did not +write the code yourself, it is your responsibility to ensure that the existing +license is compatible and included in the contributed files or you can obtain +permission from the original author to relicense the code. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index d1e0d55b44..0000000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,137 +0,0 @@ -.. _contributing: - -Contributing to SimPEG -======================= - -First of all, we are glad you are here! We welcome contributions and input -from the community. - -This document is a set of guidelines for contributing to the repositories -hosted in the `SimPEG `_ organization on GitHub. -These repositories are maintained on a volunteer basis. - -.. _questions: - -Questions -========= - -If you have a question regarding a specific use of SimPEG, the fastest way -to get a response is by posting on our Discourse discussion forum: -https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, -you can join our slack group at http://slack.simpeg.xyz. -Please do not create an issue to ask a question. - -.. _Issues: - -Issues -====== - -Issues are a place for you to suggest enhancements or raise problems you are -having with the package to the developers. - -.. _bugs: - -Bugs ----- - -When reporting an issue, please be as descriptive and provide sufficient -detail to reproduce the error. Whenever possible, if you can include a small -example that produces the error, this will help us resolve issues faster. - - -.. _suggest enhancements: - -Suggesting enhancements ------------------------ - -We welcome ideas for improvements on SimPEG! When writing an issue to suggest -an improvement, please - -- use a descriptive title, -- explain where the gap in current functionality is, -- include a pseudocode sketch of the intended functionality - -We will use the issue as a place to discuss and provide feedback. Please -remember that SimPEG is maintained on a volunteer basis. If you suggest an -enhancement, we certainly appreciate if you are also willing to take action -and start a pull request! - - -.. _pull_requests: - -Pull Requests -============= - -We welcome contributions to SimPEG in the form of pull requests (PR) - -.. _contributing_new_code: - -Contributing new code ---------------------- - -.. _getting started: https://docs.simpeg.xyz/content/basic/installing_for_developers.html - -.. _practices: https://docs.simpeg.xyz/content/basic/practices.html - -.. _testing: https://docs.simpeg.xyz/content/basic/practices.html#testing - -.. _documentation: https://docs.simpeg.xyz/content/basic/practices.html#documentation - -.. _code style: https://docs.simpeg.xyz/content/basic/practices.html#style - -If you have an idea for how to improve SimPEG, please first create an issue -and `suggest enhancements`_. We will use the -issue as a place to discuss and make decisions on the suggestion. Once you are -ready to take action and commit some code to SimPEG, please check out -`getting started`_ for -tips on setting up a development environment and `practices`_ -for a description of the development practices we aim to follow. In particular, - -- `testing`_ -- `documentation`_ -- `code style`_ - -are aspects we look for in all pull requests. We do code reviews on pull -requests, with the aim of promoting best practices and ensuring that new -contributions can be built upon by the SimPEG community. - -.. _pr_stages: - -Stages of a pull request ------------------------- - -When first creating a pull request (PR), try to make your suggested changes as tightly -scoped as possible (try to solve one problem at a time). The fewer changes you make, the faster -your branch will be merged! - -If your pull request is not ready for final review, but you still want feedback -on your current coding process please mark it as a draft pull request. Once you -feel the pull request is ready for final review, you can convert the draft PR to -an open PR by selecting the ``Ready for review`` button at the bottom of the page. - -Once a pull request is in ``open`` status and you are ready for review, please ping -the simpeg developers in a github comment ``@simpeg/simpeg-developers`` to request a -review. At minimum for a PR to be eligible to merge, we look for - -- 100% (or as close as possible) difference testing. Meaning any new code is completely tested. -- All tests are passing. -- All reviewer comments (if any) have been addressed. -- A developer approves the PR. - -After all these steps are satisfied, a ``@simpeg/simpeg-admin`` will merge your pull request into -the main branch (feel free to ping one of us on github). - -This being said, all simpeg developers and admins are essentially volunteers -providing their time for the benefit of the community. This does mean that -it might take some time for us to get your PR. - - -Licensing -========= - -All code contributed to SimPEG is licensed under the `MIT license -`_ which allows open -and commercial use and extension of SimPEG. If you did not write -the code yourself, it is your responsibility to ensure that the existing -license is compatible and included in the contributed files or you can obtain -permission from the original author to relicense the code. From a0fe322ba722240e6c8ddb1f040609556ae8d655 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:31:03 -0700 Subject: [PATCH 244/455] Remove E731 from .flake8 --- .flake8 | 2 -- 1 file changed, 2 deletions(-) diff --git a/.flake8 b/.flake8 index 8bb414e57c..7051754c09 100644 --- a/.flake8 +++ b/.flake8 @@ -88,8 +88,6 @@ ignore = E402, # comparison to None should be ‘if cond is None:’ E711, - # do not assign a lambda expression, use a def - E731, # 'from %s import *' used; unable to detect undefined names F403, # %r may be undefined, or defined from star imports: %s From 65f58b3afb3ae1773eb27534f44ed30bfaa3e2ed Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:32:21 -0700 Subject: [PATCH 245/455] Remove F403 and F405 from .flake8 --- .flake8 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.flake8 b/.flake8 index 8bb414e57c..7e845793d2 100644 --- a/.flake8 +++ b/.flake8 @@ -90,10 +90,6 @@ ignore = E711, # do not assign a lambda expression, use a def E731, - # 'from %s import *' used; unable to detect undefined names - F403, - # %r may be undefined, or defined from star imports: %s - F405, # '...'.format(...) has unused named argument(s): %s F522, # '...'.format(...) has unused arguments at position(s): %s From f63fb87424b722cc2d6472024a1aaeb13ba2094c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:33:48 -0700 Subject: [PATCH 246/455] Remove F522, F523 and F524 from .flake8 --- .flake8 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.flake8 b/.flake8 index 8bb414e57c..5df8148188 100644 --- a/.flake8 +++ b/.flake8 @@ -94,12 +94,6 @@ ignore = F403, # %r may be undefined, or defined from star imports: %s F405, - # '...'.format(...) has unused named argument(s): %s - F522, - # '...'.format(...) has unused arguments at position(s): %s - F523, - # '...'.format(...) is missing argument(s) for placeholder(s): %s - F524, # f-string is missing placeholders F541, # redefinition of unused %r from line %r From a00e1598b430dd0fb1f5693d342c4c837db522cb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:35:09 -0700 Subject: [PATCH 247/455] Remove F541 from .flake8 --- .flake8 | 2 -- 1 file changed, 2 deletions(-) diff --git a/.flake8 b/.flake8 index 8bb414e57c..49bd649fe2 100644 --- a/.flake8 +++ b/.flake8 @@ -100,8 +100,6 @@ ignore = F523, # '...'.format(...) is missing argument(s) for placeholder(s): %s F524, - # f-string is missing placeholders - F541, # redefinition of unused %r from line %r F811, # undefined name %r From bf8b327878bd223484e1c548efb45309b91e947c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:37:11 -0700 Subject: [PATCH 248/455] Fix url --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8545261c53..080b7f546e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,8 @@ First of all, we are glad you are here! We welcome contributions and input from the community. You can find guidelines on how to contribute to SimPEG in the [Contributing to -SimPEG](content/basic/contributing/index.html) section of our documentation. +SimPEG](https://docs.simpeg.xyz/content/basic/contributing/index.html) section +of our documentation. ## Licensing From 5ab702df11b3d5c8396a58928f6c448a8ff43008 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Jun 2023 15:56:40 -0700 Subject: [PATCH 249/455] Fix F811 flake error: remove redefinitions Remove F811 from list of ignored flake warnings in `.flake8`. Remove redefinitions of variables. --- .flake8 | 2 - .../natural_source/utils/data_utils.py | 1 - SimPEG/fields.py | 5 -- SimPEG/regularization/__init__.py | 2 - tests/base/test_problem.py | 1 - tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py | 1 - tests/em/fdem/forward/test_FDEM_forwardHB.py | 48 +++++++------- tests/em/fdem/forward/test_FDEM_sources.py | 62 ------------------- tests/em/tdem/test_TDEM_DerivAdjoint.py | 5 +- .../test_TDEM_DerivAdjoint_RawWaveform.py | 6 +- tests/em/tdem/test_TDEM_crosscheck.py | 16 ----- .../em/tdem/test_TDEM_inductive_permeable.py | 1 - 12 files changed, 28 insertions(+), 122 deletions(-) diff --git a/.flake8 b/.flake8 index 8bb414e57c..9546c9971b 100644 --- a/.flake8 +++ b/.flake8 @@ -102,8 +102,6 @@ ignore = F524, # f-string is missing placeholders F541, - # redefinition of unused %r from line %r - F811, # undefined name %r F821, # Block quote ends without a blank line; unexpected unindent. diff --git a/SimPEG/electromagnetics/natural_source/utils/data_utils.py b/SimPEG/electromagnetics/natural_source/utils/data_utils.py index 2e9ebf0ea3..3b5975de9e 100644 --- a/SimPEG/electromagnetics/natural_source/utils/data_utils.py +++ b/SimPEG/electromagnetics/natural_source/utils/data_utils.py @@ -6,7 +6,6 @@ import SimPEG as simpeg from SimPEG.electromagnetics.natural_source.survey import Survey, Data from SimPEG.electromagnetics.natural_source.receivers import ( - PointNaturalSource, PointNaturalSource, Point3DTipper, ) diff --git a/SimPEG/fields.py b/SimPEG/fields.py index 85178c361f..dcd5bfb8c3 100644 --- a/SimPEG/fields.py +++ b/SimPEG/fields.py @@ -114,11 +114,6 @@ def dtype(self): """ return self._dtype - @property - def knownFields(self): - """Fields known to this object.""" - return self._knownFields - @property def mesh(self): return self.simulation.mesh diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 5aa2828ab5..42c8878dc5 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -213,8 +213,6 @@ VectorAmplitude, ) -from .vector import BaseVectorRegularization, CrossReferenceRegularization - @deprecate_class(removal_version="0.19.0", future_warn=True) class SimpleSmall(Smallness): diff --git a/tests/base/test_problem.py b/tests/base/test_problem.py index d5a0f074ae..c256c627da 100644 --- a/tests/base/test_problem.py +++ b/tests/base/test_problem.py @@ -1,6 +1,5 @@ import unittest import discretize -import discretize from SimPEG import simulation import numpy as np diff --git a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py index f457765c26..7f8e92f9a4 100644 --- a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py @@ -1,7 +1,6 @@ import unittest from SimPEG import maps import SimPEG.electromagnetics.time_domain as tdem -import numpy as np from SimPEG.electromagnetics.utils import convolve_with_waveform from geoana.em.tdem import ( vertical_magnetic_flux_time_deriv_horizontal_loop as dbdt_loop, diff --git a/tests/em/fdem/forward/test_FDEM_forwardHB.py b/tests/em/fdem/forward/test_FDEM_forwardHB.py index ae98a1ec3e..a55addc608 100644 --- a/tests/em/fdem/forward/test_FDEM_forwardHB.py +++ b/tests/em/fdem/forward/test_FDEM_forwardHB.py @@ -306,7 +306,7 @@ def test_BH_CrossCheck_hzi(self): if testBH: - def test_BH_CrossCheck_jxr(self): + def test_BH_CrossCheck_jxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -318,7 +318,7 @@ def test_BH_CrossCheck_jxr(self): ) ) - def test_BH_CrossCheck_jyr(self): + def test_BH_CrossCheck_jyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -330,7 +330,7 @@ def test_BH_CrossCheck_jyr(self): ) ) - def test_BH_CrossCheck_jzr(self): + def test_BH_CrossCheck_jzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -342,7 +342,7 @@ def test_BH_CrossCheck_jzr(self): ) ) - def test_BH_CrossCheck_jxi(self): + def test_BH_CrossCheck_jxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -354,7 +354,7 @@ def test_BH_CrossCheck_jxi(self): ) ) - def test_BH_CrossCheck_jyi(self): + def test_BH_CrossCheck_jyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -366,7 +366,7 @@ def test_BH_CrossCheck_jyi(self): ) ) - def test_BH_CrossCheck_jzi(self): + def test_BH_CrossCheck_jzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -378,7 +378,7 @@ def test_BH_CrossCheck_jzi(self): ) ) - def test_BH_CrossCheck_exr(self): + def test_BH_CrossCheck_exr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -390,7 +390,7 @@ def test_BH_CrossCheck_exr(self): ) ) - def test_BH_CrossCheck_eyr(self): + def test_BH_CrossCheck_eyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -402,7 +402,7 @@ def test_BH_CrossCheck_eyr(self): ) ) - def test_BH_CrossCheck_ezr(self): + def test_BH_CrossCheck_ezr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -414,7 +414,7 @@ def test_BH_CrossCheck_ezr(self): ) ) - def test_BH_CrossCheck_exi(self): + def test_BH_CrossCheck_exi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -426,7 +426,7 @@ def test_BH_CrossCheck_exi(self): ) ) - def test_BH_CrossCheck_eyi(self): + def test_BH_CrossCheck_eyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -438,7 +438,7 @@ def test_BH_CrossCheck_eyi(self): ) ) - def test_BH_CrossCheck_ezi(self): + def test_BH_CrossCheck_ezi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -450,7 +450,7 @@ def test_BH_CrossCheck_ezi(self): ) ) - def test_BH_CrossCheck_bxr(self): + def test_BH_CrossCheck_bxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -462,7 +462,7 @@ def test_BH_CrossCheck_bxr(self): ) ) - def test_BH_CrossCheck_byr(self): + def test_BH_CrossCheck_byr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -474,7 +474,7 @@ def test_BH_CrossCheck_byr(self): ) ) - def test_BH_CrossCheck_bzr(self): + def test_BH_CrossCheck_bzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -486,7 +486,7 @@ def test_BH_CrossCheck_bzr(self): ) ) - def test_BH_CrossCheck_bxi(self): + def test_BH_CrossCheck_bxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -498,7 +498,7 @@ def test_BH_CrossCheck_bxi(self): ) ) - def test_BH_CrossCheck_byi(self): + def test_BH_CrossCheck_byi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -510,7 +510,7 @@ def test_BH_CrossCheck_byi(self): ) ) - def test_BH_CrossCheck_bzi(self): + def test_BH_CrossCheck_bzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -522,7 +522,7 @@ def test_BH_CrossCheck_bzi(self): ) ) - def test_BH_CrossCheck_hxr(self): + def test_BH_CrossCheck_hxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -534,7 +534,7 @@ def test_BH_CrossCheck_hxr(self): ) ) - def test_BH_CrossCheck_hyr(self): + def test_BH_CrossCheck_hyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -546,7 +546,7 @@ def test_BH_CrossCheck_hyr(self): ) ) - def test_BH_CrossCheck_hzr(self): + def test_BH_CrossCheck_hzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -558,7 +558,7 @@ def test_BH_CrossCheck_hzr(self): ) ) - def test_BH_CrossCheck_hxi(self): + def test_BH_CrossCheck_hxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -570,7 +570,7 @@ def test_BH_CrossCheck_hxi(self): ) ) - def test_BH_CrossCheck_hyi(self): + def test_BH_CrossCheck_hyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -582,7 +582,7 @@ def test_BH_CrossCheck_hyi(self): ) ) - def test_BH_CrossCheck_hzi(self): + def test_BH_CrossCheck_hzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index a5eb74d305..e044e51b28 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -225,26 +225,6 @@ def test_MagDipole_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_MagDipole_bPrimaryMu0_h(self): - src = fdem.sources.MagDipole( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_MagDipole_bPrimaryMu50_h(self): - src = fdem.sources.MagDipole( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_MagDipole_bPrimaryMu0_h(self): src = fdem.sources.MagDipole( [], @@ -307,26 +287,6 @@ def test_MagDipole_Bfield_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_MagDipole_Bfield_bPrimaryMu0_h(self): - src = fdem.sources.MagDipole_Bfield( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_MagDipole_Bfield_bPrimaryMu50_h(self): - src = fdem.sources.MagDipole_Bfield( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_MagDipole_Bfield_bPrimaryMu0_h(self): src = fdem.sources.MagDipole_Bfield( [], @@ -393,28 +353,6 @@ def test_CircularLoop_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_CircularLoop_bPrimaryMu0_h(self): - src = fdem.sources.CircularLoop( - [], - frequency=self.frequency, - radius=np.sqrt(1 / np.pi), - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_CircularLoop_bPrimaryMu50_h(self): - src = fdem.sources.CircularLoop( - [], - frequency=self.frequency, - radius=np.sqrt(1 / np.pi), - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_CircularLoop_bPrimaryMu0_h(self): src = fdem.sources.CircularLoop( [], diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 5433ea9a4a..8cf01a7975 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -238,9 +238,6 @@ def test_Jvec_b_dbdtx(self): def test_Jvec_b_dbdtz(self): self.JvecTest("MagneticFluxTimeDerivativez") - def test_Jvec_b_jy(self): - self.JvecTest("CurrentDensityy") - def test_Jvec_b_hx(self): self.JvecTest("MagneticFieldx") @@ -282,7 +279,7 @@ def test_Jvec_adjoint_b_hz(self): def test_Jvec_adjoint_b_dhdtx(self): self.JvecVsJtvecTest("MagneticFieldTimeDerivativex") - def test_Jvec_adjoint_b_dhdtx(self): + def test_Jvec_adjoint_b_dhdtz(self): self.JvecVsJtvecTest("MagneticFieldTimeDerivativez") def test_Jvec_adjoint_b_ey(self): diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 843fd2fa3b..0184eb9dee 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -174,13 +174,13 @@ def test_Jvec_e_ey(self): if testAdjoint: - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("MagneticFluxTimeDerivativex") - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("MagneticFluxTimeDerivativez") - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("ElectricFieldy") diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index 8f43792056..d6548bbd37 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -145,14 +145,6 @@ def test_HJ_j_stepoff(self): waveform="stepoff", ) - def test_HJ_j_stepoff(self): - CrossCheck( - prbtype1="MagneticField", - prbtype2="CurrentDensity", - rxcomp="CurrentDensityy", - waveform="stepoff", - ) - def test_HJ_dhdtx_stepoff(self): CrossCheck( prbtype1="MagneticField", @@ -219,14 +211,6 @@ def test_HJ_j_vtem(self): waveform="vtem", ) - def test_HJ_j_vtem(self): - CrossCheck( - prbtype1="MagneticField", - prbtype2="CurrentDensity", - rxcomp="CurrentDensityy", - waveform="vtem", - ) - def test_HJ_dhdtx_vtem(self): CrossCheck( prbtype1="MagneticField", diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index 254769daec..a6845b0a44 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -1,7 +1,6 @@ import unittest import discretize -from discretize import utils import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LogNorm From 4015453f5ee17524ff45347c64797e3c62b8545c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Jun 2023 14:24:03 -0700 Subject: [PATCH 250/455] Handle cell_weights attr in BaseRegularization Properly handle the deprecated `cell_weights` attribute in the `BaseRegularization` class. --- SimPEG/regularization/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 4bdf36ab41..8a64588493 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -58,6 +58,7 @@ def __init__( self._regularization_mesh = mesh self._weights = {} + # Handle deprecated indActive argument if (key := "indActive") in kwargs: if active_cells is not None: raise ValueError( @@ -71,6 +72,20 @@ def __init__( ) active_cells = kwargs.pop(key) + # Handle deprecated cell_weights argument + if (key := "cell_weights") in kwargs: + if weights is not None: + raise ValueError( + f"Cannot simultanously pass 'weights' and '{key}'. " + "Pass 'weights' only." + ) + warnings.warn( + f"The '{key}' argument has been deprecated, please use 'weights'. " + "It will be removed in future versions of SimPEG.", + DeprecationWarning, + ) + weights = kwargs.pop(key) + if active_cells is not None: self.active_cells = active_cells From 83ec65561728ccc7e90a468b0fa8b58086b1581c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Jun 2023 14:34:13 -0700 Subject: [PATCH 251/455] Reorder attribute definitions in BaseRegularization These changes just improve readability, it shouldn't change behaviour of the code. --- SimPEG/regularization/base.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 8a64588493..9c9c3efb3e 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -55,9 +55,6 @@ def __init__( f"Value of type {type(mesh)} provided." ) - self._regularization_mesh = mesh - self._weights = {} - # Handle deprecated indActive argument if (key := "indActive") in kwargs: if active_cells is not None: @@ -86,15 +83,14 @@ def __init__( ) weights = kwargs.pop(key) + super().__init__(nP=None, mapping=None, **kwargs) + self._regularization_mesh = mesh + self._weights = {} if active_cells is not None: self.active_cells = active_cells - - super().__init__(nP=None, mapping=None, **kwargs) - self.mapping = mapping # Set mapping using the setter self.reference_model = reference_model self.units = units - if weights is not None: if not isinstance(weights, dict): weights = {"user_weights": weights} From 5fd4a62d4403fa89552ee9e6d8128b8ee596b839 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Jun 2023 15:16:07 -0700 Subject: [PATCH 252/455] Add more hints about pre-commit Mark configuring pre-commit as an optional step, and add instructions on how to permanently disable it or bypass it for a single commit. Add admonition clarifying that contributors are allowed to push changes that fail to follow our code style and they can ask for help. --- .../content/basic/contributing/code-style.rst | 17 +++++++++--- .../contributing/setting-up-environment.rst | 27 +++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/docs/content/basic/contributing/code-style.rst b/docs/content/basic/contributing/code-style.rst index 2354b94d00..8a021f333a 100644 --- a/docs/content/basic/contributing/code-style.rst +++ b/docs/content/basic/contributing/code-style.rst @@ -10,12 +10,12 @@ ensure it complies with Black code style. flake8_ performs style checks, raises warnings on code that could lead towards bugs, performs checks on consistent documentation formatting, and identifies poor coding practices. -.. important:: +.. hint:: - If you :ref:`configure-pre-commit`, pre-commit_ will automatically run - Black and flake8 on every commit. + If you :ref:`configure pre-commit `, it will + automatically run Black and flake8 on every commit. -Alternatively, one could run them manually at anytime. +One can manually run Black_ and flake8_ anytime. Run ``black`` on SimPEG directories that contain Python source files: .. code:: @@ -28,6 +28,15 @@ Run ``flake8`` on the whole project with: flake8 +.. important:: + + Following code style rules can be challenging for new contributors. These + rules are meant to ease the development process, not to generate an obstacle + to contribute. Please, don't hesistate to **ask for help** if your + contribution raises some flake8 errors. And **feel free to push** code that + **don't follow our code style 100%** in :ref:`pull-requests`. Other + developers will be there to help you solve them. + .. note:: SimPEG is currently not `PEP 8 `_ diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/basic/contributing/setting-up-environment.rst index 42b830310c..08a0678bfe 100644 --- a/docs/content/basic/contributing/setting-up-environment.rst +++ b/docs/content/basic/contributing/setting-up-environment.rst @@ -101,8 +101,8 @@ You are now set up to SimPEG! .. _configure-pre-commit: -Configure pre-commit --------------------- +Configure pre-commit (optional) +------------------------------- We recommend using pre-commit_ to ensure that your new code follows the code style of SimPEG. pre-commit will run Black_ and flake8_ before any commit you @@ -112,6 +112,29 @@ make. To configure it, you need to navigate to your cloned SimPEG repo and run: pre-commit install +.. note:: + + Using ``pre-commit`` is recommended, but not necessary. You can still + manually run Black_ and flake8_. See our :ref:`code-style` page for more + details. + +If for some reason you want to stop using ``pre-commit`` on SimPEG, you can +permanently configure it to stop running automatically with: + +.. code:: + + pre-commit uninstall + +Alternatively, you can temporarily bypass ``pre-commit`` when committing some changes by running: + +.. code:: + + git commit --no-verify + +This is specially useful if the checks run by ``pre-commit`` are failing, but +you want to commit them nonetheless. + + .. _pre-commit: https://pre-commit.com/ .. _Black: https://black.readthedocs.io .. _flake8: https://flake8.pycqa.org From b3af404d61f0747e4cef811d8905a481fb2a1f1d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Jun 2023 15:40:50 -0700 Subject: [PATCH 253/455] Rename basic dir in docs to getting_started Move the `getting_started.rst` file to `getting_started/index.rst`. --- docs/content/{basic => getting_started}/big_picture.rst | 0 .../{basic => getting_started}/contributing/advanced.rst | 0 .../{basic => getting_started}/contributing/code-style.rst | 0 .../contributing/documentation.rst | 0 .../{basic => getting_started}/contributing/index.rst | 0 .../contributing/pull-requests.rst | 0 .../contributing/setting-up-environment.rst | 0 .../{basic => getting_started}/contributing/testing.rst | 0 .../contributing/working-with-github.rst | 0 .../{getting_started.rst => getting_started/index.rst} | 6 +++--- docs/content/{basic => getting_started}/installing.rst | 0 docs/index.rst | 2 +- 12 files changed, 4 insertions(+), 4 deletions(-) rename docs/content/{basic => getting_started}/big_picture.rst (100%) rename docs/content/{basic => getting_started}/contributing/advanced.rst (100%) rename docs/content/{basic => getting_started}/contributing/code-style.rst (100%) rename docs/content/{basic => getting_started}/contributing/documentation.rst (100%) rename docs/content/{basic => getting_started}/contributing/index.rst (100%) rename docs/content/{basic => getting_started}/contributing/pull-requests.rst (100%) rename docs/content/{basic => getting_started}/contributing/setting-up-environment.rst (100%) rename docs/content/{basic => getting_started}/contributing/testing.rst (100%) rename docs/content/{basic => getting_started}/contributing/working-with-github.rst (100%) rename docs/content/{getting_started.rst => getting_started/index.rst} (71%) rename docs/content/{basic => getting_started}/installing.rst (100%) diff --git a/docs/content/basic/big_picture.rst b/docs/content/getting_started/big_picture.rst similarity index 100% rename from docs/content/basic/big_picture.rst rename to docs/content/getting_started/big_picture.rst diff --git a/docs/content/basic/contributing/advanced.rst b/docs/content/getting_started/contributing/advanced.rst similarity index 100% rename from docs/content/basic/contributing/advanced.rst rename to docs/content/getting_started/contributing/advanced.rst diff --git a/docs/content/basic/contributing/code-style.rst b/docs/content/getting_started/contributing/code-style.rst similarity index 100% rename from docs/content/basic/contributing/code-style.rst rename to docs/content/getting_started/contributing/code-style.rst diff --git a/docs/content/basic/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst similarity index 100% rename from docs/content/basic/contributing/documentation.rst rename to docs/content/getting_started/contributing/documentation.rst diff --git a/docs/content/basic/contributing/index.rst b/docs/content/getting_started/contributing/index.rst similarity index 100% rename from docs/content/basic/contributing/index.rst rename to docs/content/getting_started/contributing/index.rst diff --git a/docs/content/basic/contributing/pull-requests.rst b/docs/content/getting_started/contributing/pull-requests.rst similarity index 100% rename from docs/content/basic/contributing/pull-requests.rst rename to docs/content/getting_started/contributing/pull-requests.rst diff --git a/docs/content/basic/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst similarity index 100% rename from docs/content/basic/contributing/setting-up-environment.rst rename to docs/content/getting_started/contributing/setting-up-environment.rst diff --git a/docs/content/basic/contributing/testing.rst b/docs/content/getting_started/contributing/testing.rst similarity index 100% rename from docs/content/basic/contributing/testing.rst rename to docs/content/getting_started/contributing/testing.rst diff --git a/docs/content/basic/contributing/working-with-github.rst b/docs/content/getting_started/contributing/working-with-github.rst similarity index 100% rename from docs/content/basic/contributing/working-with-github.rst rename to docs/content/getting_started/contributing/working-with-github.rst diff --git a/docs/content/getting_started.rst b/docs/content/getting_started/index.rst similarity index 71% rename from docs/content/getting_started.rst rename to docs/content/getting_started/index.rst index ecaa7745a0..6721be40a5 100644 --- a/docs/content/getting_started.rst +++ b/docs/content/getting_started/index.rst @@ -9,6 +9,6 @@ Here you'll find instructions on getting up and running with ``SimPEG``. .. toctree:: :maxdepth: 2 - basic/big_picture - basic/installing - basic/contributing/index.rst + big_picture + installing + contributing/index.rst diff --git a/docs/content/basic/installing.rst b/docs/content/getting_started/installing.rst similarity index 100% rename from docs/content/basic/installing.rst rename to docs/content/getting_started/installing.rst diff --git a/docs/index.rst b/docs/index.rst index 3cf569fb33..7f742f6860 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ :hidden: :titlesonly: - content/getting_started + content/getting_started/index content/user_guide content/api/index content/release/index From 4befdc7cbcf3008f583ad2132ab0bf7638671975 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Jun 2023 15:41:38 -0700 Subject: [PATCH 254/455] Update all links to pages in "basic" Fix links pointing to the old `basic` pages. Now they point to `getting_started`. --- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- .github/pull_request_template.md | 8 ++++---- CONTRIBUTING.md | 4 ++-- README.rst | 4 ++-- .../contributing/setting-up-environment.rst | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 318a563659..c42bef31e8 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -11,7 +11,7 @@ body: developers on [slack](https://slack.simpeg.xyz), in addition to opening an issue or pull request here. You can also check out our - [Contributor Guide](https://docs.simpeg.xyz/content/basic/contributing.html) + [Contributor Guide](https://docs.simpeg.xyz/content/getting_started/contributing.html) if you need more information. - type: textarea diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index daaf7c1746..b86a29d588 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,7 @@ Thanks for contributing a pull request to SimPEG! Remember to use a personal fork of SimPEG to propose changes. Check out the stages of a pull request at -https://docs.simpeg.xyz/content/basic/contributing.html#pull-request +https://docs.simpeg.xyz/content/getting_started/contributing.html#pull-request Note that we are a team of volunteers and we appreciate your patience during the review process. @@ -18,10 +18,10 @@ Feel free to remove lines from this template that do not apply to you pull reque #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR -* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/basic/practices.html#style). -* [ ] Added [tests](https://docs.simpeg.xyz/content/basic/practices.html#testing) to verify changes to the code. +* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/practices.html#style). +* [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the - expect [style](https://docs.simpeg.xyz/content/basic/practices.html#documentation). + expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). * [ ] Added relevant method tags (i.e. `GRAV`, `EM`, etc.) * [ ] Marked as ready for review (ff this is was a draft PR), and converted to a Pull Request diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 080b7f546e..10c0f3522b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ First of all, we are glad you are here! We welcome contributions and input from the community. You can find guidelines on how to contribute to SimPEG in the [Contributing to -SimPEG](https://docs.simpeg.xyz/content/basic/contributing/index.html) section -of our documentation. +SimPEG](https://docs.simpeg.xyz/content/getting_started/contributing/index.html) +section of our documentation. ## Licensing diff --git a/README.rst b/README.rst index 09eace6923..2f3c023ce7 100644 --- a/README.rst +++ b/README.rst @@ -159,5 +159,5 @@ Contributing We always welcome contributions towards SimPEG whether they are adding new code, suggesting improvements to existing codes, identifying bugs, providing examples, or anything that will improve SimPEG. -Please checkout the `contributing guide `_ -for more information on how to contribute. \ No newline at end of file +Please checkout the `contributing guide `_ +for more information on how to contribute. diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index 42b830310c..040733f309 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -96,7 +96,7 @@ You are now set up to SimPEG! If all is not well, please submit an issue_ and `change this file`_! .. _issue: https://github.com/simpeg/simpeg/issues -.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/basic/contributing/setting-up-environment.rst +.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/getting_started/contributing/setting-up-environment.rst .. _configure-pre-commit: From 501aa0f9c38124a34e0a5824797c0c45065e2c87 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 21 Jun 2023 16:00:02 -0700 Subject: [PATCH 255/455] fix failed tests due to overwritten `getRHSDeriv` --- SimPEG/electromagnetics/time_domain/simulation.py | 3 --- tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/SimPEG/electromagnetics/time_domain/simulation.py b/SimPEG/electromagnetics/time_domain/simulation.py index 8d414c339d..09ae43a85e 100644 --- a/SimPEG/electromagnetics/time_domain/simulation.py +++ b/SimPEG/electromagnetics/time_domain/simulation.py @@ -1056,9 +1056,6 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # assumes no source derivs return C.T * self.MfRhoDeriv(s_e, v, adjoint) - def getRHSDeriv(self, tInd, src, v, adjoint=False): - return Zero() # assumes no derivs on sources - def getAdc(self): D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index f3daba41e8..6325b2f228 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -233,11 +233,9 @@ def test_Jvec_h_hx(self): def test_Jvec_h_hz(self): self.JvecTest("MagneticFieldz") - @pytest.mark.xfail def test_Jvec_h_dhdtx(self): self.JvecTest("MagneticFieldTimeDerivativex") - @pytest.mark.xfail def test_Jvec_h_dhdtz(self): self.JvecTest("MagneticFieldTimeDerivativez") From 11ea431c145b4fa35ac63d8ac507d48ab2a9434f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 21 Jun 2023 16:05:21 -0700 Subject: [PATCH 256/455] fix incorrect f strings --- tests/em/tdem/test_TDEM_DerivAdjoint.py | 2 +- tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 100af08490..21ff5f0b4b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -133,7 +133,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print(f"{prbtype} {V1} {V2} {passed}") + print(f"{self.formulation} {V1} {V2} {passed}") self.assertTrue(passed) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 6325b2f228..9771d87a25 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -150,7 +150,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print(f"{prbtype} {V1} {V2} {passed}") + print(f"{self.formulation} {V1} {V2} {passed}") self.assertTrue(passed) From 7a8516f9d96a5044b7488cda4e5ced67e65a8dbe Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 22 Jun 2023 15:58:33 -0700 Subject: [PATCH 257/455] Remove unwanted "out" file --- out | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 out diff --git a/out b/out deleted file mode 100644 index 739071577c..0000000000 --- a/out +++ /dev/null @@ -1,8 +0,0 @@ -DCAnalytic_Pole_Dipole -DCAnalytic_Dipole_Pole -DCAnalytic_Pole_Pole -DCAnalytic_Dipole_Dipole -DCAnalyticSphere - - -AnBnfun From 41f98df78acca095e7ea7b7ed033ec34f45ec527 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 22 Jun 2023 16:25:40 -0700 Subject: [PATCH 258/455] Rename method to `..._jy` to avoid redefinition --- tests/em/tdem/test_TDEM_DerivAdjoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 34efaff2a6..1820044ca1 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -278,7 +278,7 @@ def test_Jvec_adjoint_b_dhdtx(self): def test_Jvec_adjoint_b_dhdtz(self): self.JvecVsJtvecTest("MagneticFieldTimeDerivativez") - def test_Jvec_adjoint_b_ey(self): + def test_Jvec_adjoint_b_jy(self): self.JvecVsJtvecTest("CurrentDensityy") From c12b527d21d1b11d515531f60990d86384fe7b3e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 22 Jun 2023 16:33:47 -0700 Subject: [PATCH 259/455] Use any available python3 in pre-commit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ebc8cdb588..b9f4b30ca2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,10 +3,10 @@ repos: rev: 23.1.0 hooks: - id: black - language_version: python3.10 + language_version: python3 - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 - language_version: python3.10 + language_version: python3 additional_dependencies: [flake8-bugbear, flake8-builtins, flake8-mutable, flake8-rst-docstrings, flake8-docstrings] From e3ce0c23317359688cef44977f54155e58d2dd71 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 09:47:45 -0700 Subject: [PATCH 260/455] Extend tests for cell_weights and weights --- tests/base/test_regularization.py | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 4c885481bb..ccd19102ff 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -6,6 +6,7 @@ import discretize from SimPEG import maps, objective_function, regularization, utils +from SimPEG.regularization import BaseRegularization, WeightedLeastSquares TOL = 1e-7 @@ -627,8 +628,16 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) -class TestIndActiveAndActiveCells: - """Test error after simultaneously passing indActive and active_cells.""" +class TestDeprecatedArguments: + """ + Test errors after simultaneously passing new and deprecated arguments. + + Within these arguments are: + + * ``indActive`` (replaced by ``active_cells``) + * ``cell_weights`` (replaced by ``weights``) + + """ @pytest.fixture(params=["1D", "2D", "3D"]) def mesh(self, request): @@ -644,23 +653,24 @@ def mesh(self, request): h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] return discretize.TensorMesh(h) - def test_base_regularization(self, mesh): - """Test BaseRegularization.""" + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + def test_active_cells(self, mesh, regularization_class): + """Test indActive and active_cells arguments.""" active_cells = np.ones(len(mesh), dtype=bool) msg = "Cannot simultanously pass 'active_cells' and 'indActive'." with pytest.raises(ValueError, match=msg): - regularization.BaseRegularization( + regularization_class( mesh, active_cells=active_cells, indActive=active_cells ) - def test_weighted_least_squares(self, mesh): - """Test WeightedLeastSquares.""" - active_cells = np.ones(len(mesh), dtype=bool) - msg = "Cannot simultanously pass 'active_cells' and 'indActive'." + def test_weights(self, mesh): + """Test cell_weights and weights.""" + weights = np.ones(len(mesh)) + msg = "Cannot simultanously pass 'weights' and 'cell_weights'." with pytest.raises(ValueError, match=msg): - regularization.WeightedLeastSquares( - mesh, active_cells=active_cells, indActive=active_cells - ) + BaseRegularization(mesh, weights=weights, cell_weights=weights) if __name__ == "__main__": From 205ae806b17148477fe7af51f516c386a307cb6a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 09:53:33 -0700 Subject: [PATCH 261/455] Fix flake8 warnings with new rules --- SimPEG/regularization/base.py | 1 - tests/base/test_objective_function.py | 1 - 2 files changed, 2 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 9c9c3efb3e..16423d190a 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -3,7 +3,6 @@ import numpy as np from discretize.base import BaseMesh -import warnings from typing import TYPE_CHECKING from .. import maps from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index a68c7439ec..857a80674c 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -2,7 +2,6 @@ import scipy.sparse as sp import pytest import unittest -import pytest from SimPEG import utils, maps from SimPEG import objective_function From 8cec45b2004b41474f004879d087a8bb610ef754 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 10:11:25 -0700 Subject: [PATCH 262/455] Don't import private methods --- SimPEG/electromagnetics/analytics/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/SimPEG/electromagnetics/analytics/__init__.py b/SimPEG/electromagnetics/analytics/__init__.py index 7091e7fdca..1df7acd782 100644 --- a/SimPEG/electromagnetics/analytics/__init__.py +++ b/SimPEG/electromagnetics/analytics/__init__.py @@ -2,12 +2,6 @@ from .FDEM import hzAnalyticDipoleF from .FDEMcasing import ( getKc, - _r2, - _getCasingHertzMagDipole, - _getCasingHertzMagDipoleDeriv_r, - _getCasingHertzMagDipoleDeriv_z, - _getCasingHertzMagDipole2Deriv_z_r, - _getCasingHertzMagDipole2Deriv_z_z, getCasingEphiMagDipole, getCasingHrMagDipole, getCasingHzMagDipole, From f8a82e5a9e553b50a56a6aebdec2fe06ba93041f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 10:11:43 -0700 Subject: [PATCH 263/455] Import geometric_factor for backward compatibility --- SimPEG/electromagnetics/static/resistivity/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SimPEG/electromagnetics/static/resistivity/utils.py b/SimPEG/electromagnetics/static/resistivity/utils.py index 5722b1355c..77b0a24295 100644 --- a/SimPEG/electromagnetics/static/resistivity/utils.py +++ b/SimPEG/electromagnetics/static/resistivity/utils.py @@ -5,6 +5,10 @@ from . import sources from .survey import Survey +# Import geometric_factor to make it available through +# SimPEG.resistivity.utils.geometric_factor (to ensure backward compatibility) +from ..utils import geometric_factor # noqa: F401 + def WennerSrcList(n_electrodes, a_spacing, in2D=False, plotIt=False): """ From 81315289736cacd175e9a4dc8dde662d173d5029 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 10:33:29 -0700 Subject: [PATCH 264/455] Tests invalid mapping for BaseObjectiveFunction Add test method that checks if error is raised after trying to set an invalid mapping class to a BaseObjectiveFunction. --- tests/base/test_objective_function.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 857a80674c..9dfc252acc 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -193,6 +193,18 @@ def test_updateMultipliers(self): self.assertTrue(phi(m) == phi1(m)) + def test_invalid_mapping(self): + """Test if setting mapping of wrong type raises errors.""" + + class Dummy: + pass + + phi = objective_function.L2ObjectiveFunction() + invalid_mapping = Dummy() + msg = "Invalid mapping of class 'Dummy'." + with pytest.raises(TypeError, match=msg): + phi.mapping = invalid_mapping + def test_early_exits(self): nP = 10 From 5b588c3f1b4b86b9ece32cce9fdb18dd194835e4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 10:54:32 -0700 Subject: [PATCH 265/455] Test invalid parent in BaseRegularization --- SimPEG/regularization/base.py | 8 ++++++-- tests/base/test_regularization.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 16423d190a..0c7a18976f 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -171,8 +171,12 @@ def parent(self): @parent.setter def parent(self, parent): - if not isinstance(parent, ComboObjectiveFunction): - raise TypeError("Parent must be a ComboObjectiveFunction") + combo_class = ComboObjectiveFunction + if not isinstance(parent, combo_class): + raise TypeError( + f"Invalid parent of type '{parent.__class__.__name__}'. " + f"Parent must be a {combo_class.__name__}." + ) self._parent = parent @property diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index ccd19102ff..37d0346e6b 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -628,6 +628,20 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) +def test_invalid_parent(): + """Test setting an invalid parent class to a BaseRegularization.""" + + class Dummy: + pass + + mesh = discretize.TensorMesh([3, 4, 5]) + reg = BaseRegularization(mesh) + invalid_parent = Dummy() + msg = "Invalid parent of type 'Dummy'." + with pytest.raises(TypeError, match=msg): + reg.parent = invalid_parent + + class TestDeprecatedArguments: """ Test errors after simultaneously passing new and deprecated arguments. From 82e9efc320f277508c88cfab30c4ce16bfc5c5b0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 11:15:31 -0700 Subject: [PATCH 266/455] Test invalid and empty objfcts in ComboObjectiveFunction --- tests/base/test_objective_function.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 9dfc252acc..1280245b8d 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -407,5 +407,42 @@ def test_add_and_mul(self, unpack_on_add): assert combo_2.objfcts == [phi3, combo_1] +@pytest.mark.parametrize( + "objfcts, multipliers", + ( + (None, None), + ([objective_function.L2ObjectiveFunction()], None), + ([objective_function.L2ObjectiveFunction()], [2.5]), + ), +) +def test_empty_combo(objfcts, multipliers): + """Test defining an empty ComboObjectiveFunction.""" + combo = objective_function.ComboObjectiveFunction( + objfcts=objfcts, multipliers=multipliers + ) + if objfcts is None and multipliers is None: + assert combo.objfcts == [] + assert combo.multipliers == [] + if objfcts is not None: + assert combo.objfcts == objfcts + if multipliers is None: + assert combo.multipliers == [1] + else: + assert combo.multipliers == [2.5] + + +def test_invalid_objfcts_in_combo(): + """Test invalid objective function class in ComboObjectiveFunction.""" + + class Dummy: + pass + + phi = objective_function.L2ObjectiveFunction() + invalid_phi = Dummy() + msg = "Unrecognized objective function type Dummy in 'objfcts'." + with pytest.raises(TypeError, match=msg): + objective_function.ComboObjectiveFunction(objfcts=[phi, invalid_phi]) + + if __name__ == "__main__": unittest.main() From 85bbb8e38a675d0f12f29b8004a5e5c1e69d5996 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 11:20:36 -0700 Subject: [PATCH 267/455] Fix W291 and W293 flake errors: white spaces Remove `W291` and `W293` from list of ignored rules in `.flake8`. Remove trailing whitespaces and whitespaces in empty lines. --- .flake8 | 4 ---- tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py | 2 +- tutorials/08-tdem/plot_fwd_2_tem_cyl.py | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.flake8 b/.flake8 index 6a06da83ff..a8b3a5429d 100644 --- a/.flake8 +++ b/.flake8 @@ -118,10 +118,6 @@ ignore = RST307, # Previously unseen severe error, not yet assigned a unique code. RST499, - # trailing whitespace - W291, - # blank line contains whitespace - W293, # Configure flake8-rst-docstrings diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py index 945b22ffa6..66085d1804 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py @@ -6,7 +6,7 @@ souce waveforms. In this tutorial, we construct a set of waveforms of different types and simulate the response for a halfspace. Many types of waveforms can be constructed within *SimPEG.electromagnetics.time_domain_1d*. These include: - + - the unit step off waveform - a set of basic waveforms: rectangular, triangular, quarter sine, etc... - a set of system-specific waveforms: SkyTEM, VTEM, GeoTEM, etc... diff --git a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py index bb9c76fed1..c7bcccde1e 100644 --- a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py +++ b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py @@ -3,7 +3,7 @@ ================================================================== Here we use the module *SimPEG.electromagnetics.time_domain* to simulate the -transient response for borehole survey using a cylindrical mesh and a +transient response for borehole survey using a cylindrical mesh and a radially symmetric conductivity. For this tutorial, we focus on the following: - How to define the transmitters and receivers From 7e5936d84a171245ec2931b024d7a55021030be6 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 23 Jun 2023 11:53:32 -0700 Subject: [PATCH 268/455] Always calculate gzz if needed Instead of using gzz = -gxx - gyy, which needs to be adjusted if inside a cell. --- SimPEG/potential_fields/gravity/simulation.py | 19 +++++++++++-------- .../potential_fields/magnetics/simulation.py | 13 +++++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index 88fa0eeb59..bc6740e0e3 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -144,14 +144,17 @@ def evaluate_integral(self, receiver_location, components): # (NN - EE) / 2 inside_adjust = False if "gzz" in components: - if "gxx" not in node_evals or "gyy" not in node_evals: - node_evals["gzz"] = prism_fzz(dx, dy, dz) - else: - inside_adjust = True - # The below need to be adjusted for observation points within a cell. - # because `gxx + gyy + gzz = -4 * pi * G * rho` - # gzz = - gxx - gyy - 4 * np.pi * G * rho[in_cell] - node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] + node_evals["gzz"] = prism_fzz(dx, dy, dz) + # The below should be uncommented when we are able to give the index of a + # containing cell. + # if "gxx" not in node_evals or "gyy" not in node_evals: + # node_evals["gzz"] = prism_fzz(dx, dy, dz) + # else: + # inside_adjust = True + # # The below need to be adjusted for observation points within a cell. + # # because `gxx + gyy + gzz = -4 * pi * G * rho` + # # gzz = - gxx - gyy - 4 * np.pi * G * rho[in_cell] + # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] rows = {} for component in set(components): diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/SimPEG/potential_fields/magnetics/simulation.py index 32144d4d3d..4b13b8ae18 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/SimPEG/potential_fields/magnetics/simulation.py @@ -269,10 +269,15 @@ def evaluate_integral(self, receiver_location, components): node_evals["gxz"] = prism_fzy(dy, dz, dx) if "gyz" not in node_evals: node_evals["gyz"] = prism_fzy(dx, dy, dz) - if "gxx" not in node_evals or "gyy" not in node_evals: - node_evals["gzz"] = prism_fzz(dx, dy, dz) - else: - node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] + node_evals["gzz"] = prism_fzz(dx, dy, dz) + # the below will be uncommented when we give the containing cell index + # for interior observations. + # if "gxx" not in node_evals or "gyy" not in node_evals: + # node_evals["gzz"] = prism_fzz(dx, dy, dz) + # else: + # # This is the one that would need to be adjusted if the observation is + # # inside an active cell. + # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] if "bxx" in components: node_evals["gxxx"] = prism_fzzz(dy, dz, dx) From f0dd3281075e12ceec0c44e98fd9ed1d0e03c83d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 14:54:09 -0700 Subject: [PATCH 269/455] Fix B028 flake error: non-explicit stacklevel Remove B028 from the list of ignored rules in `.flake`. Explicitly set stacklevel on `warning.warn` calls. --- .flake8 | 2 -- SimPEG/data.py | 3 +- SimPEG/directives/directives.py | 29 +++++++++++++------ .../frequency_domain/receivers.py | 3 +- .../frequency_domain/sources.py | 3 +- .../static/resistivity/IODC.py | 7 ++++- .../static/resistivity/simulation_2d.py | 3 +- .../static/utils/static_utils.py | 8 +++-- .../electromagnetics/time_domain/receivers.py | 3 +- .../electromagnetics/time_domain/sources.py | 3 +- .../electromagnetics/utils/current_utils.py | 4 ++- SimPEG/maps.py | 4 +-- SimPEG/regularization/base.py | 3 ++ SimPEG/regularization/pgi.py | 4 ++- SimPEG/simulation.py | 2 +- SimPEG/survey.py | 3 +- SimPEG/utils/code_utils.py | 24 +++++++-------- .../io_utils/io_utils_electromagnetics.py | 12 +++++--- SimPEG/utils/model_utils.py | 2 ++ SimPEG/utils/pgi_utils.py | 1 + SimPEG/utils/plot_utils.py | 1 + SimPEG/utils/solver_utils.py | 12 +++++--- 22 files changed, 90 insertions(+), 46 deletions(-) diff --git a/.flake8 b/.flake8 index 6a06da83ff..f0e7f87cda 100644 --- a/.flake8 +++ b/.flake8 @@ -36,8 +36,6 @@ exclude-from-doctest = ignore = # assertRaises(Exception): should be considered evil B017, - # No explicit stacklevel argument found. - B028, # Missing docstring in public module D100, # Missing docstring in public class diff --git a/SimPEG/data.py b/SimPEG/data.py index 5a45d2ed9b..4ba5ca3571 100644 --- a/SimPEG/data.py +++ b/SimPEG/data.py @@ -76,7 +76,8 @@ def __init__( if relative_error is not None or noise_floor is not None: warnings.warn( "Setting the standard_deviation overwrites the " - "relative_error and noise_floor" + "relative_error and noise_floor", + stacklevel=2, ) self.standard_deviation = standard_deviation diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index e9ceb80e18..c44e8d2915 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -110,7 +110,8 @@ def inversion(self, i): warnings.warn( "InversionDirective {0!s} has switched to a new inversion.".format( self.__class__.__name__ - ) + ), + stacklevel=2, ) self._inversion = i @@ -308,7 +309,10 @@ def inversion(self, i): return if getattr(self, "_inversion", None) is not None: warnings.warn( - "{0!s} has switched to a new inversion.".format(self.__class__.__name__) + "{0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, ) for d in self.dList: d.inversion = i @@ -1297,7 +1301,8 @@ def initialize(self): ] if smallness[smallness[:, 2] == 1][:, :2].size == 0: warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)" + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", + stacklevel=2, ) self.smallness = -1 self.pgi_smallness = None @@ -1340,7 +1345,8 @@ def initialize(self): if smallness[smallness[:, 1] == 1][:, :1].size == 0: if self.TriggerSmall: warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)." + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", + stacklevel=2, ) self.TriggerSmall = False self.smallness = -1 @@ -2248,7 +2254,8 @@ def validate(self, directiveList): warnings.warn( "Without a Linear preconditioner, convergence may be slow. " "Consider adding `Directives.UpdatePreconditioner` to your " - "directives list" + "directives list", + stacklevel=2, ) return True @@ -2531,21 +2538,24 @@ def __init__( if "everyIter" in kwargs.keys(): warnings.warn( "'everyIter' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'every_iteration'." + "Please use 'every_iteration'.", + stacklevel=2, ) every_iteration = kwargs.pop("everyIter") if "threshold" in kwargs.keys(): warnings.warn( "'threshold' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'threshold_value'." + "Please use 'threshold_value'.", + stacklevel=2, ) threshold_value = kwargs.pop("threshold") if "normalization" in kwargs.keys(): warnings.warn( "'normalization' property is deprecated and will be removed in SimPEG 0.20.0." - "Please define normalization using 'normalization_method'." + "Please define normalization using 'normalization_method'.", + stacklevel=2, ) normalization_method = kwargs.pop("normalization") if normalization_method is True: @@ -2661,7 +2671,8 @@ def normalization_method(self, value): elif isinstance(value, bool): warnings.warn( "Boolean type for 'normalization_method' is deprecated and will be removed in 0.20.0." - "Please use None, 'maximum' or 'minimum'." + "Please use None, 'maximum' or 'minimum'.", + stacklevel=2, ) if value: self._normalization_method = "maximum" diff --git a/SimPEG/electromagnetics/frequency_domain/receivers.py b/SimPEG/electromagnetics/frequency_domain/receivers.py index a8b6d4e256..b6423da9a0 100644 --- a/SimPEG/electromagnetics/frequency_domain/receivers.py +++ b/SimPEG/electromagnetics/frequency_domain/receivers.py @@ -38,7 +38,8 @@ def __init__( warnings.warn( "'projComp' overrides the 'orientation' property which automatically" " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0." + "'projComp' is deprecated and will be removed in SimPEG 0.19.0.", + stacklevel=2, ) self.projComp = proj diff --git a/SimPEG/electromagnetics/frequency_domain/sources.py b/SimPEG/electromagnetics/frequency_domain/sources.py index 74e121b215..bc54e0fdf4 100644 --- a/SimPEG/electromagnetics/frequency_domain/sources.py +++ b/SimPEG/electromagnetics/frequency_domain/sources.py @@ -841,7 +841,8 @@ def moment(self, value): if value is not None: warnings.warn( "Moment is not set as a property. I is the product" - "of the loop radius and transmitter current" + "of the loop radius and transmitter current", + stacklevel=2, ) pass diff --git a/SimPEG/electromagnetics/static/resistivity/IODC.py b/SimPEG/electromagnetics/static/resistivity/IODC.py index 5c7c6c3432..2fc42de988 100644 --- a/SimPEG/electromagnetics/static/resistivity/IODC.py +++ b/SimPEG/electromagnetics/static/resistivity/IODC.py @@ -95,7 +95,10 @@ def __init__( self.pad_rate_z = pad_rate_z self.ncell_per_dipole = ncell_per_dipole self.corezlength = corezlength - warnings.warn("code under construction - API might change in the future") + warnings.warn( + "code under construction - API might change in the future", + stacklevel=2, + ) @property def survey_layout(self): @@ -959,6 +962,7 @@ def set_mesh( "Because the x coordinates of some topo and electrodes are the same," " we excluded electrodes with the same coordinates.", RuntimeWarning, + stacklevel=2, ) locs_tmp = np.vstack((topo, self.electrode_locations[~mask, :])) row_idx = np.lexsort((locs_tmp[:, 0],)) @@ -973,6 +977,7 @@ def set_mesh( "Because the x and y coordinates of some topo and electrodes are the same," " we excluded electrodes with the same coordinates.", RuntimeWarning, + stacklevel=2, ) locs_tmp = np.vstack((topo, self.electrode_locations[~mask, :])) row_idx = np.lexsort((locs_tmp[:, 1], locs_tmp[:, 0])) diff --git a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py b/SimPEG/electromagnetics/static/resistivity/simulation_2d.py index 58e57047dc..3d7423790d 100644 --- a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py +++ b/SimPEG/electromagnetics/static/resistivity/simulation_2d.py @@ -99,7 +99,8 @@ def g(k): if not out["success"]: warnings.warn( "Falling back to trapezoidal for integration. " - "You may need to change nky." + "You may need to change nky.", + stacklevel=2, ) do_trap = True if do_trap: diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/SimPEG/electromagnetics/static/utils/static_utils.py index b522e21194..886ec6a167 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/SimPEG/electromagnetics/static/utils/static_utils.py @@ -204,6 +204,7 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): "The keyword arguments of this function have been deprecated." " All of the necessary information is now in the DC survey class", DeprecationWarning, + stacklevel=2, ) # Pre-allocate @@ -562,7 +563,9 @@ def plot_pseudosection( if kwarg in kwargs: raise TypeError(r"The {kwarg} keyword has been removed.") if len(kwargs) > 0: - warnings.warn("plot_pseudosection unused kwargs: {list(kwargs.keys())}") + warnings.warn( + f"plot_pseudosection unused kwargs: {list(kwargs.keys())}", stacklevel=2 + ) if plot_type.lower() not in ["pcolor", "contourf", "scatter"]: raise ValueError( @@ -1061,7 +1064,8 @@ def generate_survey_from_abmn_locations( warnings.warn( "Ordering of ABMN locations changed when generating survey. " "Associated data vectors will need sorting. Set output_sorting to " - "True for sorting indices." + "True for sorting indices.", + stacklevel=2, ) if output_sorting: diff --git a/SimPEG/electromagnetics/time_domain/receivers.py b/SimPEG/electromagnetics/time_domain/receivers.py index 49e9e78c32..3179c527af 100644 --- a/SimPEG/electromagnetics/time_domain/receivers.py +++ b/SimPEG/electromagnetics/time_domain/receivers.py @@ -32,7 +32,8 @@ def __init__( warnings.warn( "'projComp' overrides the 'orientation' property which automatically" " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0." + "'projComp' is deprecated and will be removed in SimPEG 0.19.0.", + stacklevel=2, ) self.projComp = proj diff --git a/SimPEG/electromagnetics/time_domain/sources.py b/SimPEG/electromagnetics/time_domain/sources.py index 04df66c7ab..fa37081259 100644 --- a/SimPEG/electromagnetics/time_domain/sources.py +++ b/SimPEG/electromagnetics/time_domain/sources.py @@ -1633,7 +1633,8 @@ def moment(self, value): if value is not None: warnings.warn( "Moment is not set as a property. I is the product" - "of the loop radius and transmitter current" + "of the loop radius and transmitter current", + stacklevel=2, ) pass diff --git a/SimPEG/electromagnetics/utils/current_utils.py b/SimPEG/electromagnetics/utils/current_utils.py index 84eb36c7ba..959f6e1260 100644 --- a/SimPEG/electromagnetics/utils/current_utils.py +++ b/SimPEG/electromagnetics/utils/current_utils.py @@ -318,7 +318,9 @@ def _poly_line_source_tree(mesh, locs): srcCellIds = mesh.get_cells_along_line(A, B) levels = mesh.cell_levels_by_index(srcCellIds) if isinstance(levels, np.ndarray) and np.any(levels != levels[0]): - warnings.warn("Warning! Line path crosses a cell level change.") + warnings.warn( + "Warning! Line path crosses a cell level change.", stacklevel=2 + ) # Starts at point A! p0 = A diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 443774b246..5cc526b0a0 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -1775,7 +1775,7 @@ def _sc2phaseEMTSpheroidstransform(self, phi1): """ if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): - warnings.warn("there are phis outside bounds of 0 and 1") + warnings.warn("there are phis outside bounds of 0 and 1", stacklevel=2) phi1 = np.median(np.c_[phi1 * 0, phi1, phi1 * 0 + 1.0]) phi0 = 1.0 - phi1 @@ -1812,7 +1812,7 @@ def _sc2phaseEMTSpheroidstransform(self, phi1): sige1 = sige2 # TODO: make this a proper warning, and output relevant info (sigma0, sigma1, phi, sigstart, and relerr) - warnings.warn("Maximum number of iterations reached") + warnings.warn("Maximum number of iterations reached", stacklevel=2) return sige2 diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 8e3104cf50..cb6a5eade9 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -232,6 +232,7 @@ def cell_weights(self) -> np.ndarray: "cell_weights are deprecated please access weights using the `set_weights`," " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", FutureWarning, + stacklevel=2, ) return np.prod(list(self._weights.values()), axis=0) @@ -241,6 +242,7 @@ def cell_weights(self, value): "cell_weights are deprecated please access weights using the `set_weights`," " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", FutureWarning, + stacklevel=2, ) self.set_weights(cell_weights=value) @@ -831,6 +833,7 @@ def cell_weights(self, value): "cell_weights are deprecated please access weights using the `set_weights`," " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", FutureWarning, + stacklevel=2, ) self.set_weights(cell_weights=value) diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 45d8f43556..26075bc647 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -74,7 +74,9 @@ def __init__( if "mapping" in kwargs: warnings.warn( - f"Property 'mapping' of class {type(self)} cannot be set. Defaults to IdentityMap." + f"Property 'mapping' of class {type(self)} cannot be set. " + "Defaults to IdentityMap.", + stacklevel=2, ) kwargs.pop("mapping") diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index 4cefb30e9b..bd45647c62 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -499,7 +499,7 @@ def G(self): if getattr(self, "_G", None) is not None: return self._G else: - warnings.warn("G has not been implemented for the simulation") + warnings.warn("G has not been implemented for the simulation", stacklevel=2) return None @G.setter diff --git a/SimPEG/survey.py b/SimPEG/survey.py index 6ad4760892..dad847cff4 100644 --- a/SimPEG/survey.py +++ b/SimPEG/survey.py @@ -40,7 +40,8 @@ def __init__(self, locations, storeProjections=False, **kwargs): if projGLoc is not None: warnings.warn( "'projGLoc' is no longer of property of the receiver class. It is set automatically " - "based on the receiver and simulation class. Will be remove in SimPEG 0.18.0" + "based on the receiver and simulation class. Will be remove in SimPEG 0.18.0", + stacklevel=2, ) # ideally there shouldn't be any kwargs left to hit, but this will throw an # error if kwargs hasn't been emptied properly. This also will allow proper diff --git a/SimPEG/utils/code_utils.py b/SimPEG/utils/code_utils.py index 3057431bb3..149d981e66 100644 --- a/SimPEG/utils/code_utils.py +++ b/SimPEG/utils/code_utils.py @@ -546,11 +546,11 @@ def decorator(cls): def __init__(self, *args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) self._old__init__(*args, **kwargs) cls.__init__ = __init__ @@ -589,11 +589,11 @@ def deprecate_module( message += " It will be removed in a future version of SimPEG." message += " Please update your code accordingly." if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) def deprecate_property( @@ -639,20 +639,20 @@ def deprecate_property( def get_dep(self): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return prop.fget(self) def set_dep(self, other): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) prop.fset(self, other) doc = f"`{old_name}` has been deprecated. See `{new_name}` for documentation" @@ -698,11 +698,11 @@ def deprecate_method( def new_method(*args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return method(*args, **kwargs) doc = f"`{old_name}` has been deprecated. See `{new_name}` for documentation" @@ -752,11 +752,11 @@ def deprecate_function( def dep_function(*args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return new_function(*args, **kwargs) doc = f""" diff --git a/SimPEG/utils/io_utils/io_utils_electromagnetics.py b/SimPEG/utils/io_utils/io_utils_electromagnetics.py index 25d833535d..552f72fbb6 100644 --- a/SimPEG/utils/io_utils/io_utils_electromagnetics.py +++ b/SimPEG/utils/io_utils/io_utils_electromagnetics.py @@ -148,7 +148,8 @@ def read_dcip_xyz( warnings.warn( "Loaded data are in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) else: locations_a = data_array[:, a_cols] @@ -315,7 +316,8 @@ def read_dcip2d_ubc(file_name, data_type, format_type): warnings.warn( "Loaded data did not have elevations. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) else: @@ -410,7 +412,8 @@ def read_dcip2d_ubc(file_name, data_type, format_type): warnings.warn( "Loaded data were in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) return data_out @@ -569,7 +572,8 @@ def read_dcip3d_ubc(file_name, data_type): warnings.warn( "Loaded data were in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) return data_out diff --git a/SimPEG/utils/model_utils.py b/SimPEG/utils/model_utils.py index 2933c9180d..8c6d19b1ab 100644 --- a/SimPEG/utils/model_utils.py +++ b/SimPEG/utils/model_utils.py @@ -39,6 +39,7 @@ def surface2ind_topo(mesh, topo, gridLoc="CC", method="nearest", fill_value=np.n "The surface2ind_topo function has been deprecated, please import " "discretize.utils.active_from_xyz. This will be removed in SimPEG 0.20.0", FutureWarning, + stacklevel=2, ) active_cells = active_from_xyz(mesh, topo, gridLoc, method) @@ -164,6 +165,7 @@ def depth_weighting( "The indActive keyword argument has been deprecated, please use active_cells. " "This will be removed in SimPEG 0.19.0", FutureWarning, + stacklevel=2, ) active_cells = kwargs["indActive"] diff --git a/SimPEG/utils/pgi_utils.py b/SimPEG/utils/pgi_utils.py index d0019eeead..3304b36d4a 100644 --- a/SimPEG/utils/pgi_utils.py +++ b/SimPEG/utils/pgi_utils.py @@ -1171,6 +1171,7 @@ def fit_predict(self, X, y=None, debug=False): "or increase max_iter, tol " "or check for degenerate data." % (init + 1), ConvergenceWarning, + stacklevel=2, ) self._set_parameters(best_params) diff --git a/SimPEG/utils/plot_utils.py b/SimPEG/utils/plot_utils.py index f847e3dfe7..fc50246fd8 100644 --- a/SimPEG/utils/plot_utils.py +++ b/SimPEG/utils/plot_utils.py @@ -368,6 +368,7 @@ def plotLayer( warnings.warn( "plotLayer has been deprecated, please use plot_1d_layer_model", DeprecationWarning, + stacklevel=2, ) thicknesses = np.diff(LocSigZ) z0 = LocSigZ[0] diff --git a/SimPEG/utils/solver_utils.py b/SimPEG/utils/solver_utils.py index 820c07fc64..12bd945b00 100644 --- a/SimPEG/utils/solver_utils.py +++ b/SimPEG/utils/solver_utils.py @@ -15,7 +15,7 @@ def _checkAccuracy(A, b, X, accuracyTol): nrm, accuracyTol ) print(msg) - warnings.warn(msg, RuntimeWarning) + warnings.warn(msg, RuntimeWarning, stacklevel=2) def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name=None): @@ -87,7 +87,8 @@ def __init__(self, A, **kwargs): culled_args[item] = kwargs[item] else: warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored" + f"{item} is not a valid keyword for {fun.__name__} and will be ignored", + stacklevel=2, ) kwargs = culled_args @@ -204,7 +205,8 @@ def __init__(self, A, **kwargs): culled_args[item] = kwargs[item] else: warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored" + f"{item} is not a valid keyword for {fun.__name__} and will be ignored", + stacklevel=2, ) kwargs = culled_args @@ -289,7 +291,9 @@ def __init__(self, A, **kwargs): self.A = A self._diagonal = A.diagonal() for kwarg in kwargs: - warnings.warn(f"{kwarg} is not recognized and will be ignored") + warnings.warn( + f"{kwarg} is not recognized and will be ignored", stacklevel=2 + ) def __mul__(self, rhs): n = self.A.shape[0] From d542e5106a30d22ef962aee9bab086e2b9a97766 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 Jun 2023 15:01:48 -0700 Subject: [PATCH 270/455] Use setter for mapping in BaseObjectiveFunction --- SimPEG/objective_function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index fa0c6d3f08..1697b27e8a 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -32,7 +32,7 @@ def __init__( debug=False, ): self._nP = nP - self._mapping = mapping + self.mapping = mapping self.counter = counter self.debug = debug self.has_fields = has_fields From d3800a63508f8054f3f6f6793736186768691252 Mon Sep 17 00:00:00 2001 From: dccowan Date: Mon, 26 Jun 2023 10:36:28 -0700 Subject: [PATCH 271/455] Post PGI regularization docstring comments --- SimPEG/regularization/pgi.py | 64 +++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index bf279d668f..05f65433e8 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -54,9 +54,11 @@ class PGIsmallness(Smallness): gmmref : SimPEG.utils.WeightedGaussianMixture Reference Gaussian mixture model. gmm : None, SimPEG.utils.WeightedGaussianMixture - Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the - inversion. If set, the Gaussian mixture model used to constrain the recovered model is - static throughout the inversion. + Set the Gaussian mixture model used to constrain the recovered physical property model. + Can be left static throughout the inversion or updated using the + :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. wiresmap : None, SimPEG.maps.Wires Mapping from the model to the model parameters of each type. If ``None``, we assume only a single physical property type in the inversion. @@ -67,7 +69,7 @@ class PGIsmallness(Smallness): property values. mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. Implemented for - `tensor`, `QuadTree` or `Octree` meshes. + ``tensor``, ``QuadTree`` or ``Octree`` meshes. approx_gradient : bool If ``True``, use the L2-approximation of the gradient by assuming physical property values of different types are uncorrelated. @@ -116,7 +118,7 @@ class PGIsmallness(Smallness): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} - When the `approx_eval` property is ``True``, we assume the physical property types have + When the ``approx_eval`` property is ``True``, we assume the physical property types have values that are uncorrelated. In this case, the weighting matrix is diagonal and the regularization function (objective function) can be expressed as: @@ -124,7 +126,7 @@ class PGIsmallness(Smallness): \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 - When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property + When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` property to ``True`` so that the least-squares approximation is used to compute the gradient. **Constructing the Reference Model and Weighting Matrix:** @@ -144,7 +146,7 @@ class PGIsmallness(Smallness): diag \big ( \mathbf{v \odot w} \big ) where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` - are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal + are custom cell weights. When the ``approx_eval`` property is ``True``, the off-diagonal covariances are zero and we can use a weighting matrix of the form: .. math:: @@ -156,9 +158,9 @@ class PGIsmallness(Smallness): **Updating the Gaussian Mixture Model:** - When the GMM is set using the `gmm` property, the GMM remains static throughout the inversion. - When the `gmm` property set as ``None``, the GMM is learned and updated after every model update. - That is, we assume the GMM defined using the `gmmref` property is not completely representative + When the GMM is set using the ``gmm`` property, the GMM remains static throughout the inversion. + When the ``gmm`` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the ``gmmref`` property is not completely representative of the physical property distributions for each rock unit, and we update the all of the means (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: @@ -265,12 +267,16 @@ def set_weights(self, **weights): def gmm(self): """Gaussian mixture model. + If set prior to inversion, the Gaussian mixture model can be left static throughout + the inversion, or updated using the :class:`.directives.PGI_UpdateParameters` directive. + If this property is not set prior to inversion, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + Returns ------- - None, SimPEG.utils.WeightedGaussianMixture - Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the - inversion. If set, the Gaussian mixture model used to constrain the recovered model is - static throughout the inversion. + SimPEG.utils.WeightedGaussianMixture + Gaussian mixture model used to constrain the recovered physical property model. """ if getattr(self, "_gmm", None) is None: self._gmm = copy.deepcopy(self.gmmref) @@ -990,9 +996,11 @@ class PGI(ComboObjectiveFunction): gmmref : SimPEG.utils.WeightedGaussianMixture Reference Gaussian mixture model. gmm : None, SimPEG.utils.WeightedGaussianMixture - Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the - inversion. If set, the Gaussian mixture model used to constrain the recovered model is - static throughout the inversion. + Set the Gaussian mixture model used to constrain the recovered physical property model. + Can be left static throughout the inversion or updated using the + :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. alpha_pgi : float Scaling constant for the PGI smallness term. alpha_x, alpha_y, alpha_z : float or None, optional @@ -1067,7 +1075,7 @@ class PGI(ComboObjectiveFunction): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} - When the `approx_eval` property is ``True``, we assume the physical property types have + When the ``approx_eval`` property is ``True``, we assume the physical property types have values that are uncorrelated. In this case, the weighting matrix is diagonal and the regularization function (objective function) can be expressed as: @@ -1078,7 +1086,7 @@ class PGI(ComboObjectiveFunction): &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - When the `approx_eval` property is ``True``, you may also set the `approx_gradient` property + When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` property to ``True`` so that the least-squares approximation is used to compute the gradient. **Constructing the Reference Model and Weighting Matrix:** @@ -1098,7 +1106,7 @@ class PGI(ComboObjectiveFunction): diag \big ( \mathbf{v \odot w} \big ) where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` - are custom cell weights. When the `approx_eval` property is ``True``, the off-diagonal + are custom cell weights. When the ``approx_eval`` property is ``True``, the off-diagonal covariances are zero and we can use a weighting matrix of the form: .. math:: @@ -1110,9 +1118,9 @@ class PGI(ComboObjectiveFunction): **Updating the Gaussian Mixture Model:** - When the GMM is set using the `gmm` property, the GMM remains static throughout the inversion. - When the `gmm` property set as ``None``, the GMM is learned and updated after every model update. - That is, we assume the GMM defined using the `gmmref` property is not completely representative + When the GMM is set using the ``gmm`` property, the GMM remains static throughout the inversion. + When the ``gmm`` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the ``gmmref`` property is not completely representative of the physical property distributions for each rock unit, and we update the all of the means (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: @@ -1237,12 +1245,16 @@ def alpha_pgi(self, value): def gmm(self): """Gaussian mixture model. + If set prior to inversion, the Gaussian mixture model can be left static throughout + the inversion, or updated using the :class:`.directives.PGI_UpdateParameters` directive. + If this property is not set prior to inversion, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + Returns ------- None, SimPEG.utils.WeightedGaussianMixture - Gaussian mixture model. If ``None``, the Gaussian mixture model is learned throughout the - inversion. If set, the Gaussian mixture model used to constrain the recovered model is - static throughout the inversion. + Gaussian mixture model. """ return self.objfcts[0].gmm From 5e3c081837afeebe2677017d7b410cafc32b9d65 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 26 Jun 2023 11:47:20 -0700 Subject: [PATCH 272/455] Set mapping through setter only if it's not None --- SimPEG/objective_function.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 1697b27e8a..400d94d2c5 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -32,7 +32,10 @@ def __init__( debug=False, ): self._nP = nP - self.mapping = mapping + if mapping is None: + self._mapping = mapping + else: + self.mapping = mapping self.counter = counter self.debug = debug self.has_fields = has_fields From ea6232f65a7f0c1255ba065133dab0a86ca3d74a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 26 Jun 2023 12:04:08 -0700 Subject: [PATCH 273/455] Solve recent flake8 non-ignored rule Specify stacklevels to new warnings. --- SimPEG/regularization/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 75e353fd6e..62daf85466 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -65,6 +65,7 @@ def __init__( f"The '{key}' argument has been deprecated, please use 'active_cells'. " "It will be removed in future versions of SimPEG.", DeprecationWarning, + stacklevel=2, ) active_cells = kwargs.pop(key) @@ -79,6 +80,7 @@ def __init__( f"The '{key}' argument has been deprecated, please use 'weights'. " "It will be removed in future versions of SimPEG.", DeprecationWarning, + stacklevel=2, ) weights = kwargs.pop(key) @@ -769,6 +771,7 @@ def __init__( f"The '{key}' argument has been deprecated, please use 'active_cells'. " "It will be removed in future versions of SimPEG.", DeprecationWarning, + stacklevel=2, ) active_cells = kwargs.pop(key) From 24de8cfb2ec95e6cf30fdb9bda8ef962506db1e8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 26 Jun 2023 15:48:26 -0700 Subject: [PATCH 274/455] Test the parent setter for BaseRegularizations --- tests/base/test_regularization.py | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 37d0346e6b..b03376a0ab 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -7,6 +7,7 @@ import discretize from SimPEG import maps, objective_function, regularization, utils from SimPEG.regularization import BaseRegularization, WeightedLeastSquares +from SimPEG.objective_function import ComboObjectiveFunction TOL = 1e-7 @@ -628,18 +629,31 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) -def test_invalid_parent(): - """Test setting an invalid parent class to a BaseRegularization.""" +class TestParent: + """Test parent property of regularizations.""" - class Dummy: - pass + @pytest.fixture + def regularization(self): + """Sample regularization instance.""" + mesh = discretize.TensorMesh([3, 4, 5]) + return BaseRegularization(mesh) - mesh = discretize.TensorMesh([3, 4, 5]) - reg = BaseRegularization(mesh) - invalid_parent = Dummy() - msg = "Invalid parent of type 'Dummy'." - with pytest.raises(TypeError, match=msg): - reg.parent = invalid_parent + def test_parent(self, regularization): + """Test setting a parent class to a BaseRegularization.""" + combo = ComboObjectiveFunction() + regularization.parent = combo + assert regularization.parent == combo + + def test_invalid_parent(self, regularization): + """Test setting an invalid parent class to a BaseRegularization.""" + + class Dummy: + pass + + invalid_parent = Dummy() + msg = "Invalid parent of type 'Dummy'." + with pytest.raises(TypeError, match=msg): + regularization.parent = invalid_parent class TestDeprecatedArguments: From 5c0b902b2416ead809833c21d34d952a13c6b9ef Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 4 Jul 2023 11:25:57 -0700 Subject: [PATCH 275/455] Make units of gravity simulations more explicit (#1256) * Make units of gravity simulations more explicit Add admonitions to the `Point` receiver for gravity simulations. Fix unit in the plot legend of one of the examples. * Explicit expected units of the density model * Add admonitions to gravity.Simulation3DIntegral Include the admonitions regarding model units and returned units to the SimPEG.potential_fields.gravity.Simulation3DIntegral class. Co-authored-by: John Weis --- SimPEG/potential_fields/gravity/receivers.py | 18 ++++++++++++++++++ SimPEG/potential_fields/gravity/simulation.py | 13 +++++++++++++ .../03-gravity/plot_1b_gravity_gradiometry.py | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/SimPEG/potential_fields/gravity/receivers.py b/SimPEG/potential_fields/gravity/receivers.py index c144800fe6..48fe680205 100644 --- a/SimPEG/potential_fields/gravity/receivers.py +++ b/SimPEG/potential_fields/gravity/receivers.py @@ -9,6 +9,20 @@ class Point(survey.BaseRx): field that are simulated at each location. The length of the resulting data vector is *n_loc X n_comp*, and is organized by location then component. + .. important:: + + Density model is assumed to be in g/cc. + + .. important:: + + Acceleration components ("gx", "gy", "gz") are returned in mgal + (:math:`10^{-5} m/s^2`). + + .. important:: + + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + returned in Eotvos (:math:`10^{-9} s^{-2}`). + Parameters ---------- locations: (n_loc, 3) numpy.ndarray @@ -28,6 +42,10 @@ class Point(survey.BaseRx): - "gyz" --> z-derivative of the y-component (and visa versa) - "gzz" --> z-derivative of the z-component - "guv" --> UV component + + See also + -------- + SimPEG.potential_fields.gravity.Simulation3DIntegral """ def __init__(self, locations, components="gz", **kwargs): diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index bc6740e0e3..8eb90a641c 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -14,6 +14,19 @@ class Simulation3DIntegral(BasePFSimulation): """ Gravity simulation in integral form. + .. important:: + + Density model is assumed to be in g/cc. + + .. important:: + + Acceleration components ("gx", "gy", "gz") are returned in mgal + (:math:`10^{-5} m/s^2`). + + .. important:: + + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + returned in Eotvos (:math:`10^{-9} s^{-2}`). """ rho, rhoMap, rhoDeriv = props.Invertible("Density") diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 8c4dd756c8..5d2c12bc93 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -265,6 +265,6 @@ cbar = mpl.colorbar.ColorbarBase( ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr ) -cbar.set_label("$mgal/m$", rotation=270, labelpad=15, size=12) +cbar.set_label("Eotvos", rotation=270, labelpad=15, size=12) plt.show() From 95dec8538bb1901d72cb83f96bab239ed4cb7b92 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 5 Jul 2023 10:13:37 -0700 Subject: [PATCH 276/455] unpack the data misfits for plotting tikhonov curves --- SimPEG/directives/directives.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index c44e8d2915..de38474488 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -1765,7 +1765,9 @@ def plot_misfit_curves( plot_small=False, plot_smooth=False, ): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + self.target_misfit = ( + np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) / 2.0 + ) self.i_target = None if self.invProb.phi_d < self.target_misfit: From 1bf729ac2807c8caf9d39e4e4ddd734c9a7373eb Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 5 Jul 2023 10:50:57 -0700 Subject: [PATCH 277/455] Update SimPEG/electromagnetics/frequency_domain/fields.py Co-authored-by: Joseph Capriotti --- SimPEG/electromagnetics/frequency_domain/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/SimPEG/electromagnetics/frequency_domain/fields.py index f0e0a50255..06900f5dc3 100644 --- a/SimPEG/electromagnetics/frequency_domain/fields.py +++ b/SimPEG/electromagnetics/frequency_domain/fields.py @@ -845,7 +845,7 @@ def _j(self, bSolution, source_list): return self._MeI * j else: - return self._MeI * self._MeSigma * self._e(bSolution, source_list) + return self._MeI * (self._MeSigma * self._e(bSolution, source_list)) def _jDeriv_u(self, src, du_dm_v, adjoint=False): """ From a8c699880390e849e7fe42a83d09e5af8f0e14db Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 5 Jul 2023 10:51:16 -0700 Subject: [PATCH 278/455] Update SimPEG/electromagnetics/frequency_domain/simulation.py Co-authored-by: Joseph Capriotti --- SimPEG/electromagnetics/frequency_domain/simulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index a94350056b..8e8e7b106c 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -613,7 +613,6 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): ############################################################################### -@with_property_mass_matrices("permittivity") class Simulation3DCurrentDensity(BaseFDEMSimulation): r""" We eliminate :math:`mathbf{h}` using From de4ccc60308391137e16ded5580d641ddd6702f0 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 5 Jul 2023 10:51:28 -0700 Subject: [PATCH 279/455] Update SimPEG/electromagnetics/frequency_domain/simulation.py Co-authored-by: Joseph Capriotti --- SimPEG/electromagnetics/frequency_domain/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 8e8e7b106c..28f58a55d9 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -66,7 +66,7 @@ def __init__( self.forward_only = forward_only if permittivity is not None: warnings.warn( - "Simulations using permittivity have not yet been thoroughly tested yet, and derivatives are not implemented. Contributions welcome!" + "Simulations using permittivity have not yet been thoroughly tested, and derivatives are not implemented. Contributions welcome!" ) self.permittivity = permittivity From d799bb8410026cc7a784aa9013174d60fb3c0f5c Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 6 Jul 2023 08:56:46 -0700 Subject: [PATCH 280/455] remove unused import --- SimPEG/electromagnetics/frequency_domain/simulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 28f58a55d9..c418a4be6a 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -4,7 +4,6 @@ from ... import props from ...data import Data -from ...base.pde_simulation import with_property_mass_matrices from ...utils import mkvc, validate_type from ..base import BaseEMSimulation from ..utils import omega From 86ccb8a70a9080376afa65a874a086cfca24ae7a Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 6 Jul 2023 09:00:43 -0700 Subject: [PATCH 281/455] update warning stacklevel based on flake8 --- SimPEG/electromagnetics/frequency_domain/simulation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index c418a4be6a..0c4ac340a4 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -65,7 +65,8 @@ def __init__( self.forward_only = forward_only if permittivity is not None: warnings.warn( - "Simulations using permittivity have not yet been thoroughly tested, and derivatives are not implemented. Contributions welcome!" + "Simulations using permittivity have not yet been thoroughly tested and derivatives are not implemented. Contributions welcome!" + stacklevel=2, ) self.permittivity = permittivity From 0f543aae6b566ae8b42015737c8ebf07024e1806 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 6 Jul 2023 09:24:42 -0700 Subject: [PATCH 282/455] fix missed comma in warning --- SimPEG/electromagnetics/frequency_domain/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 0c4ac340a4..50b138cf9e 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -65,7 +65,7 @@ def __init__( self.forward_only = forward_only if permittivity is not None: warnings.warn( - "Simulations using permittivity have not yet been thoroughly tested and derivatives are not implemented. Contributions welcome!" + "Simulations using permittivity have not yet been thoroughly tested and derivatives are not implemented. Contributions welcome!", stacklevel=2, ) self.permittivity = permittivity From deb5b88a89c8de711588a8660199209e57974748 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Fri, 7 Jul 2023 09:22:18 -0700 Subject: [PATCH 283/455] Update pull_request_template.md (#1258) - suggest removing that we ask contributors to add tags (only members of the SimPEG org are able to do this, new contributors are not). This is up for discussion! happy to revert if folks prefer - small typo in marking for review --- .github/pull_request_template.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b86a29d588..06d6b74691 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,8 +22,7 @@ Feel free to remove lines from this template that do not apply to you pull reque * [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). -* [ ] Added relevant method tags (i.e. `GRAV`, `EM`, etc.) -* [ ] Marked as ready for review (ff this is was a draft PR), and converted +* [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. @@ -39,4 +38,4 @@ Feel free to remove lines from this template that do not apply to you pull reque \ No newline at end of file +--> From dc194f6b58c7609f68f86ca4d337167532cd7885 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 7 Jul 2023 10:23:00 -0700 Subject: [PATCH 284/455] Anisotropy derivative support (#1237) * fix for anisotropy models, beginings of test * add ability to work with a function input to the inner matmul operation * variable name * add tests, and fix implementation * Address comments. * raise error for unexpected stashed property derivative. (and add test for it) --- SimPEG/base/pde_simulation.py | 127 ++++++++++---- tests/base/test_mass_matrices.py | 274 ++++++++++++++++++++++++++++++- 2 files changed, 363 insertions(+), 38 deletions(-) diff --git a/SimPEG/base/pde_simulation.py b/SimPEG/base/pde_simulation.py index a38ad4a467..a213d922ad 100644 --- a/SimPEG/base/pde_simulation.py +++ b/SimPEG/base/pde_simulation.py @@ -1,6 +1,6 @@ import numpy as np import scipy.sparse as sp -from discretize.utils import Zero +from discretize.utils import Zero, TensorType from ..simulation import BaseSimulation from .. import props from scipy.constants import mu_0 @@ -8,34 +8,70 @@ def __inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) - if v is not None: - if v.ndim > 1: - v = np.squeeze(v) - if u.ndim > 1: - # u has multiple fields - if v.ndim == 1: - v = v[:, None] - else: + if sp.issparse(M): + if v is not None: if v.ndim > 1: - u = u[:, None] - if v.ndim > 2: - u = u[:, None, :] - if adjoint: - if u.ndim > 1 and u.shape[-1] > 1: - return M.T * (u * v).sum(axis=-1) - return M.T * (u * v) - if u.ndim > 1 and u.shape[1] > 1: - return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) - return u * (M * v) - else: + v = np.squeeze(v) + if u.ndim > 1: + # u has multiple fields + if v.ndim == 1: + v = v[:, None] + else: + if v.ndim > 1: + u = u[:, None] + if v.ndim > 2: + u = u[:, None, :] + if adjoint: + if u.ndim > 1 and u.shape[-1] > 1: + return M.T * (u * v).sum(axis=-1) + return M.T * (u * v) + if u.ndim > 1 and u.shape[1] > 1: + return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) + return u * (M * v) + else: + if u.ndim > 1: + UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + else: + U = sp.diags(u, format="csr") + UM = U @ M + if adjoint: + return UM.T + return UM + elif isinstance(M, tuple): + # assume it was a tuple of M_func, prop_deriv + M_deriv_func, prop_deriv = M if u.ndim > 1: - UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + Mu = [M_deriv_func(u[:, i]) for i in range(u.shape[1])] + if v is None: + Mu = sp.vstack([M @ prop_deriv for M in Mu]) + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return sum( + [prop_deriv.T @ (Mu[i].T @ v[..., i]) for i in range(u.shape[1])] + ) + pv = prop_deriv @ v + return np.stack([M @ pv for M in Mu], axis=-1) else: - U = sp.diags(u, format="csr") - UM = U @ M - if adjoint: - return UM.T - return UM + Mu = M_deriv_func(u) + if v is None: + Mu = Mu @ prop_deriv + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return prop_deriv.T @ (Mu.T @ v) + return Mu @ (prop_deriv @ v) + else: + raise TypeError( + "The stashed property derivative is an unexpected type. Expected either a `tuple` or a " + f"sparse matrix. Received a {type(M)}." + ) def with_property_mass_matrices(property_name): @@ -232,10 +268,22 @@ def MfDeriv_prop(self, u, v=None, adjoint=False): return Zero() stash_name = f"_Mf_{arg}_deriv" if getattr(self, stash_name, None) is None: - M_prop_deriv = self.mesh.get_face_inner_product_deriv( - np.ones(self.mesh.n_cells) - )(np.ones(self.mesh.n_faces)) * getattr(self, f"{arg.lower()}Deriv") - setattr(self, stash_name, M_prop_deriv) + prop = getattr(self, arg.lower()) + t_type = TensorType(self.mesh, prop) + + M_deriv_func = self.mesh.get_face_inner_product_deriv(model=prop) + prop_deriv = getattr(self, f"{arg.lower()}Deriv") + # t_type == 3 for full tensor model, t_type < 3 for scalar, isotropic, or axis-aligned anisotropy. + if t_type < 3 and self.mesh._meshType.lower() in ( + "cyl", + "tensor", + "tree", + ): + M_prop_deriv = M_deriv_func(np.ones(self.mesh.n_faces)) @ prop_deriv + setattr(self, stash_name, M_prop_deriv) + else: + setattr(self, stash_name, (M_deriv_func, prop_deriv)) + return __inner_mat_mul_op( getattr(self, stash_name), u, v=v, adjoint=adjoint ) @@ -252,10 +300,21 @@ def MeDeriv_prop(self, u, v=None, adjoint=False): return Zero() stash_name = f"_Me_{arg}_deriv" if getattr(self, stash_name, None) is None: - M_prop_deriv = self.mesh.get_edge_inner_product_deriv( - np.ones(self.mesh.n_cells) - )(np.ones(self.mesh.n_edges)) * getattr(self, f"{arg.lower()}Deriv") - setattr(self, stash_name, M_prop_deriv) + prop = getattr(self, arg.lower()) + t_type = TensorType(self.mesh, prop) + + M_deriv_func = self.mesh.get_edge_inner_product_deriv(model=prop) + prop_deriv = getattr(self, f"{arg.lower()}Deriv") + # t_type == 3 for full tensor model, t_type < 3 for scalar, isotropic, or axis-aligned anisotropy. + if t_type < 3 and self.mesh._meshType.lower() in ( + "cyl", + "tensor", + "tree", + ): + M_prop_deriv = M_deriv_func(np.ones(self.mesh.n_edges)) @ prop_deriv + setattr(self, stash_name, M_prop_deriv) + else: + setattr(self, stash_name, (M_deriv_func, prop_deriv)) return __inner_mat_mul_op( getattr(self, stash_name), u, v=v, adjoint=adjoint ) diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_mass_matrices.py index 396a75cd29..7dd1d82ebc 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_mass_matrices.py @@ -6,6 +6,8 @@ from scipy.constants import mu_0 from discretize.tests import check_derivative from discretize.utils import Zero +import scipy.sparse as sp +import pytest # define a very simple class... @@ -41,9 +43,24 @@ def setUp(self): self.mesh = discretize.TensorMesh([5, 6, 7]) self.sim = SimpleSim(self.mesh, sigmaMap=maps.ExpMap()) - self.start_mod = np.log(1e-2 * np.ones(self.mesh.n_cells)) + np.random.randn( - self.mesh.n_cells - ) + n_cells = self.mesh.n_cells + self.start_mod = np.log(np.full(n_cells, 1e-2)) + np.random.randn(n_cells) + self.start_diag_mod = np.r_[ + np.log(np.full(n_cells, 1e-2)), + np.log(np.full(n_cells, 2e-2)), + np.log(np.full(n_cells, 3e-2)), + ] + np.random.randn(3 * n_cells) + + self.sim_full_aniso = SimpleSim(self.mesh, sigmaMap=maps.IdentityMap()) + + self.start_full_mod = np.r_[ + np.full(n_cells, 1), + np.full(n_cells, 2), + np.full(n_cells, 3), + np.full(n_cells, -1), + np.full(n_cells, 1), + np.full(n_cells, -2), + ] def test_zero_returns(self): n_c = self.mesh.n_cells @@ -180,6 +197,66 @@ def test_forward_expected_shapes(self): UM @ v, sim.MfSigmaDeriv(u2, v).reshape(-1, order="F") ) + def test_forward_anis_expected_shapes(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + # if U.shape (*, ) + u = np.random.rand(n_f) + v = np.random.randn(n_p) + u2 = np.random.rand(n_f, 2) + v2 = np.random.randn(n_p, 4) + + # These cases should all return an array of shape (n_f, ) + # if V.shape (*, ) + out = sim.MfSigmaDeriv(u, v) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u, v[:, None]) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u[:, None], v) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u[:, None], v[:, None]) + assert out.shape == (n_f,) + + # now check passing multiple V's + out = sim.MfSigmaDeriv(u, v2) + assert out.shape == (n_f, 4) + out = sim.MfSigmaDeriv(u[:, None], v2) + assert out.shape == (n_f, 4) + + # also ensure it properly broadcasted the operation.... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u[:, None], v2[:, i]) + np.testing.assert_equal(out, out_2) + + # now check for multiple source polarizations + out = sim.MfSigmaDeriv(u2, v) + assert out.shape == (n_f, 2) + out = sim.MfSigmaDeriv(u2, v[:, None]) + assert out.shape == (n_f, 2) + + # and with multiple RHS + out = sim.MfSigmaDeriv(u2, v2) + assert out.shape == (n_f, v2.shape[1], 2) + + # and test broadcasting here... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i, :] = sim.MfSigmaDeriv(u2, v2[:, i]) + np.testing.assert_equal(out, out_2) + + # test None as v + UM = sim.MfSigmaDeriv(u) + np.testing.assert_allclose(UM @ v, sim.MfSigmaDeriv(u, v)) + + UM = sim.MfSigmaDeriv(u2) + np.testing.assert_allclose( + UM @ v, sim.MfSigmaDeriv(u2, v).reshape(-1, order="F") + ) + def test_adjoint_expected_shapes(self): sim = self.sim sim.model = self.start_mod @@ -242,7 +319,69 @@ def test_adjoint_expected_shapes(self): UMT @ v2_2.reshape(-1, order="F"), sim.MfSigmaDeriv(u2, v2_2, adjoint=True) ) - def test_adjoint_opp_shapes(self): + def test_adjoint_anis_expected_shapes(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + + u = np.random.rand(n_f) + v = np.random.randn(n_f) + v2 = np.random.randn(n_f, 4) + u2 = np.random.rand(n_f, 2) + v2_2 = np.random.randn(n_f, 2) + v3 = np.random.rand(n_f, 4, 2) + + # These cases should all return an array of shape (n_c, ) + # if V.shape (n_f, ) + out = sim.MfSigmaDeriv(u, v, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u, v[:, None], adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u[:, None], v, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u[:, None], v[:, None], adjoint=True) + assert out.shape == (n_p,) + + # now check passing multiple V's + out = sim.MfSigmaDeriv(u, v2, adjoint=True) + assert out.shape == (n_p, 4) + out = sim.MfSigmaDeriv(u[:, None], v2, adjoint=True) + assert out.shape == (n_p, 4) + + # also ensure it properly broadcasted the operation.... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u, v2[:, i], adjoint=True) + np.testing.assert_equal(out, out_2) + + # now check for multiple source polarizations + out = sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + assert out.shape == (n_p,) + + # and with multiple RHS + out = sim.MfSigmaDeriv(u2, v3, adjoint=True) + assert out.shape == (n_p, v3.shape[1]) + + # and test broadcasting here... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u2, v3[:, i, :], adjoint=True) + np.testing.assert_equal(out, out_2) + + # test None as v + UMT = sim.MfSigmaDeriv(u, adjoint=True) + np.testing.assert_allclose(UMT @ v, sim.MfSigmaDeriv(u, v, adjoint=True)) + + UMT = sim.MfSigmaDeriv(u2, adjoint=True) + np.testing.assert_allclose( + UMT @ v2_2.reshape(-1, order="F"), sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + ) + + def test_adjoint_opp(self): sim = self.sim sim.model = self.start_mod @@ -301,6 +440,44 @@ def test_adjoint_opp_shapes(self): yJtv = np.sum(y2 * sim.MfSigmaIDeriv(u2, v3, adjoint=True)) np.testing.assert_allclose(vJy, yJtv) + def test_anis_adjoint_opp(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + + u = np.random.rand(n_f) + u2 = np.random.rand(n_f, 2) + + y = np.random.rand(n_p) + y2 = np.random.rand(n_p, 4) + + v = np.random.randn(n_f) + v2 = np.random.randn(n_f, 4) + v2_2 = np.random.randn(n_f, 2) + v3 = np.random.rand(n_f, 4, 2) + + # u1, y1 -> v1 + vJy = v @ sim.MfSigmaDeriv(u, y) + yJtv = y @ sim.MfSigmaDeriv(u, v, adjoint=True) + np.testing.assert_allclose(vJy, yJtv) + + # u1, y2 -> v2 + vJy = np.sum(v2 * sim.MfSigmaDeriv(u, y2)) + yJtv = np.sum(y2 * sim.MfSigmaDeriv(u, v2, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + + # u2, y1 -> v2_2 + vJy = np.sum(v2_2 * sim.MfSigmaDeriv(u2, y)) + yJtv = np.sum(y * sim.MfSigmaDeriv(u2, v2_2, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + + # u2, y2 -> v3 + vJy = np.sum(v3 * sim.MfSigmaDeriv(u2, y2)) + yJtv = np.sum(y2 * sim.MfSigmaDeriv(u2, v3, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + def test_Mcc_deriv(self): u = np.random.randn(self.mesh.n_cells) sim = self.sim @@ -352,6 +529,40 @@ def Jvec(v): assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Me_diagonal_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_edges) + sim = self.sim + x0 = self.start_diag_mod + + def f(x): + sim.model = x + d = sim.MeSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MeSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + + def test_Me_full_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_edges) + sim = self.sim_full_aniso + x0 = self.start_full_mod + + def f(x): + sim.model = x + d = sim.MeSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MeSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Mf_deriv(self): u = np.random.randn(self.mesh.n_faces) sim = self.sim @@ -369,6 +580,40 @@ def Jvec(v): assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Mf_diagonal_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_faces) + sim = self.sim + x0 = self.start_diag_mod + + def f(x): + sim.model = x + d = sim.MfSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MfSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + + def test_Mf_full_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_faces) + sim = self.sim_full_aniso + x0 = self.start_full_mod + + def f(x): + sim.model = x + d = sim.MfSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MfSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_MccI_deriv(self): u = np.random.randn(self.mesh.n_cells) sim = self.sim @@ -540,3 +785,24 @@ def test_MfI_adjoint(self): yJv = y @ sim.MfSigmaIDeriv(u, v) vJty = v @ sim.MfSigmaIDeriv(u, y, adjoint=True) np.testing.assert_allclose(yJv, vJty) + + +def test_bad_derivative_stash(): + mesh = discretize.TensorMesh([5, 6, 7]) + sim = SimpleSim(mesh, sigmaMap=maps.ExpMap()) + sim.model = np.random.rand(mesh.n_cells) + + u = np.random.rand(mesh.n_edges) + v = np.random.rand(mesh.n_cells) + + # This should work + sim.MeSigmaDeriv(u, v) + # stashed derivative operation is a sparse matrix + assert sp.issparse(sim._Me_Sigma_deriv) + + # Let's set the stashed item as a bad value which would error + # The user shouldn't cause this to happen, but a developer might. + sim._Me_Sigma_deriv = [40, 10, 30] + + with pytest.raises(TypeError): + sim.MeSigmaDeriv(u, v) From e38d6b652ebdd6e131b2c0b7deb8ca6e6d64324c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sat, 8 Jul 2023 07:34:29 -0700 Subject: [PATCH 285/455] update geoana minimum version --- environment_test.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment_test.yml b/environment_test.yml index 59d59bc19f..8e96ef045e 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -8,7 +8,7 @@ dependencies: - pymatsolver>=0.2 - matplotlib - discretize>=0.8 - - geoana>=0.4.0 + - geoana>=0.5.0 - empymod>=2.0.0 - pandas - dask diff --git a/setup.py b/setup.py index ba1da29111..675dcf6839 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ "pymatsolver>=0.2", "matplotlib", "discretize>=0.8", - "geoana>=0.4.0", + "geoana>=0.5.0", "empymod>=2.0.0", "pandas", ], From 2a4dff7c412c5481513f93bedd9018dbe9efbf59 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 11 Jul 2023 10:23:11 -0700 Subject: [PATCH 286/455] optionally import utm (#1260) --- .../natural_source/utils/edi_files_utils.py | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py b/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py index b4383b442e..d70c19429a 100644 --- a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py +++ b/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py @@ -2,14 +2,20 @@ from SimPEG import mkvc from numpy.lib import recfunctions as recFunc from .data_utils import rec_to_ndarr +from discretize.utils import requires # Import modules import numpy as np import os import re -import utm +try: + import utm +except ImportError: + utm = False + +@requires({"utm": utm}) class EDIimporter: """ A class to import EDIfiles. @@ -86,7 +92,7 @@ def importFiles(self): # Find the location latD, longD, elevM = _findLatLong(EDIlines) # Transfrom coordinates - transCoord = self._transfromPoints(longD, latD) + transCoord = utm.from_latlon(latD, longD) # Extract the name of the file (station) EDIname = EDIfile.split(os.sep)[-1].split(".")[0] # Arrange the data @@ -128,37 +134,6 @@ def importFiles(self): # Assign the data self._data = outTemp - # % Assign the data to the obj - # nOutData=length(obj.data); - # obj.data(nOutData+1:nOutData+length(TEMP.data),:) = TEMP.data; - def _transfromPoints(self, longD, latD): - # Import the coordinate projections - # try: - # import osr - # except ImportError as e: - # print( - # ( - # "Could not import osr, missing the gdal" - # + "package\nCan not project coordinates" - # ) - # ) - # raise e - # # Coordinates convertor - # if self._2out is None: - # src = osr.SpatialReference() - # src.ImportFromEPSG(4326) - # out = osr.SpatialReference() - # if self._outEPSG is None: - # # Find the UTM EPSG number - # Nnr = 700 if latD < 0.0 else 600 - # utmZ = int(1 + (longD + 180.0) / 6.0) - # self._outEPSG = 32000 + Nnr + utmZ - # out.ImportFromEPSG(self._outEPSG) - # self._2out = osr.CoordinateTransformation(src, out) - # # Return the transfrom - # return self._2out.TransformPoint(longD, latD) - return utm.from_latlon(latD, longD) - # Hidden functions def _findLatLong(fileLines): From 07085d952bb1af3a32545d7f1a5d0c8bb348da31 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 13 Jul 2023 09:27:01 -0700 Subject: [PATCH 287/455] Set storage type of pf sensitivity matrix (#1261) * Make dtype an option for the sensitivity matrix. This also avoids double storing the sensitivity matrix when concatenating it. * add test * reduce tolerance * set copy=false on astype calls --- SimPEG/dask/potential_fields/base.py | 2 +- SimPEG/potential_fields/base.py | 55 ++++++++++++++++--- SimPEG/potential_fields/gravity/simulation.py | 13 +++-- .../potential_fields/magnetics/simulation.py | 19 +++++-- tests/meta/test_meta_sim.py | 8 +-- tests/pf/test_forward_Grav_Linear.py | 16 +++++- 6 files changed, 88 insertions(+), 25 deletions(-) diff --git a/SimPEG/dask/potential_fields/base.py b/SimPEG/dask/potential_fields/base.py index 1df42c82a5..7e5c96c06b 100644 --- a/SimPEG/dask/potential_fields/base.py +++ b/SimPEG/dask/potential_fields/base.py @@ -16,7 +16,7 @@ def dask_linear_operator(self): rows = [ array.from_delayed( row(receiver_location, components), - dtype=np.float32, + dtype=self.sensitivity_dtype, shape=(len(components),) if forward_only else (len(components), n_cells), ) for receiver_location, components in self.survey._location_component_iterator() diff --git a/SimPEG/potential_fields/base.py b/SimPEG/potential_fields/base.py index cb57ac3c26..753a80f8fd 100644 --- a/SimPEG/potential_fields/base.py +++ b/SimPEG/potential_fields/base.py @@ -72,6 +72,7 @@ def __init__( ind_active=None, store_sensitivities="ram", n_processes=1, + sensitivity_dtype=np.float32, **kwargs, ): # If deprecated property set with kwargs @@ -86,6 +87,7 @@ def __init__( ) self.store_sensitivities = store_sensitivities + self.sensitivity_dtype = sensitivity_dtype super().__init__(mesh, **kwargs) self.solver = None self.n_processes = n_processes @@ -155,6 +157,27 @@ def store_sensitivities(self, value): "store_sensitivities", value, ["disk", "ram", "forward_only"] ) + @property + def sensitivity_dtype(self): + """dtype of the sensitivity matrix. + + Returns + ------- + numpy.float32 or numpy.float64 + The dtype used to store the sensitivity matrix + """ + if self.store_sensitivities == "forward_only": + return np.float64 + return self._sensitivity_dtype + + @sensitivity_dtype.setter + def sensitivity_dtype(self, value): + if value is not np.float32 and value is not np.float64: + raise TypeError( + "sensitivity_dtype must be either np.float32 or np.float64." + ) + self._sensitivity_dtype = value + @property def n_processes(self): return self._n_processes @@ -204,20 +227,36 @@ def linear_operator(self): print(f"Found sensitivity file at {sens_name} with expected shape") kernel = np.asarray(kernel) return kernel + if self.store_sensitivities == "forward_only": + kernel_shape = (self.survey.nD,) + else: + kernel_shape = (self.survey.nD, n_cells) + dtype = self.sensitivity_dtype + kernel = np.empty(kernel_shape, dtype=dtype) if self.n_processes == 1: - kernel = [] + id0 = 0 for args in self.survey._location_component_iterator(): - kernel.append(self.evaluate_integral(*args)) + rows = self.evaluate_integral(*args) + n_c = rows.shape[0] + id1 = id0 + n_c + kernel[id0:id1] = rows.astype(dtype, copy=False) + id0 = id1 else: # multiprocessed with Pool(processes=self.n_processes) as pool: - kernel = pool.starmap( + id0 = 0 + for rows in pool.starmap( self.evaluate_integral, self.survey._location_component_iterator() - ) - if self.store_sensitivities != "forward_only": - kernel = np.vstack(kernel) - else: - kernel = np.concatenate(kernel) + ): + n_c = rows.shape[0] + id1 = id0 + n_c + kernel[id0:id1] = rows.astype(dtype, copy=False) + id0 = id1 + + # if self.store_sensitivities != "forward_only": + # kernel = np.vstack(kernel) + # else: + # kernel = np.concatenate(kernel) if self.store_sensitivities == "disk": print(f"writing sensitivity to {sens_name}") os.makedirs(self.sensitivity_path, exist_ok=True) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index 8eb90a641c..c5f134e402 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -46,7 +46,7 @@ def fields(self, m): # Compute the linear operation without forming the full dense G fields = mkvc(self.linear_operator()) else: - fields = self.G @ (self.rho).astype(np.float32) + fields = self.G @ (self.rho).astype(self.sensitivity_dtype, copy=False) return np.asarray(fields) @@ -80,13 +80,13 @@ def Jvec(self, m, v, f=None): Sensitivity times a vector """ dmu_dm_v = self.rhoDeriv @ v - return self.G @ dmu_dm_v.astype(np.float32) + return self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) def Jtvec(self, m, v, f=None): """ Sensitivity transposed times a vector """ - Jtvec = self.G.T @ v.astype(np.float32) + Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) return np.asarray(self.rhoDeriv.T @ Jtvec) @property @@ -198,7 +198,12 @@ def evaluate_integral(self, receiver_location, components): else: rows[component] *= constants.G * 1e8 # conversion for mGal - return np.stack([rows[component] for component in components]) + return np.stack( + [ + rows[component].astype(self.sensitivity_dtype, copy=False) + for component in components + ] + ) class SimulationEquivalentSourceLayer( diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/SimPEG/potential_fields/magnetics/simulation.py index 4b13b8ae18..8d926d5e4a 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/SimPEG/potential_fields/magnetics/simulation.py @@ -108,7 +108,9 @@ def fields(self, model): if self.store_sensitivities == "forward_only": fields = mkvc(self.linear_operator()) else: - fields = np.asarray(self.G @ self.chi.astype(np.float32)) + fields = np.asarray( + self.G @ self.chi.astype(self.sensitivity_dtype, copy=False) + ) if self.is_amplitude_data: fields = self.compute_amplitude(fields) @@ -182,7 +184,7 @@ def Jvec(self, m, v, f=None): self.model = m dmu_dm_v = self.chiDeriv @ v - Jvec = self.G @ dmu_dm_v.astype(np.float32) + Jvec = self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) if self.is_amplitude_data: # dask doesn't support an "order" argument to reshape... @@ -199,13 +201,15 @@ def Jtvec(self, m, v, f=None): v = self.ampDeriv * v # dask doesn't support and "order" argument to reshape... v = v.T.reshape(-1) # .reshape(-1, order="F") - Jtvec = self.G.T @ v.astype(np.float32) + Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) return np.asarray(self.chiDeriv.T @ Jtvec) @property def ampDeriv(self): if getattr(self, "_ampDeriv", None) is None: - fields = np.asarray(self.G.dot(self.chi).astype(np.float32)) + fields = np.asarray( + self.G.dot(self.chi).astype(self.sensitivity_dtype, copy=False) + ) self._ampDeriv = self.normalized_fields(fields) return self._ampDeriv @@ -428,7 +432,12 @@ def evaluate_integral(self, receiver_location, components): rows[component] /= 4 * np.pi - return np.stack([rows[component] for component in components]) + return np.stack( + [ + rows[component].astype(self.sensitivity_dtype, copy=False) + for component in components + ] + ) @property def deleteTheseOnModelUpdate(self): diff --git a/tests/meta/test_meta_sim.py b/tests/meta/test_meta_sim.py index f5df798439..c0a0ccf660 100644 --- a/tests/meta/test_meta_sim.py +++ b/tests/meta/test_meta_sim.py @@ -133,26 +133,26 @@ def test_sum_sim_correctness(): # test fields objects f_full = full_sim.fields(m_test) f_mult = sum_sim.fields(m_test) - np.testing.assert_allclose(f_full, sum(f_mult)) + np.testing.assert_allclose(f_full, sum(f_mult), rtol=1e-6) # test data output d_full = full_sim.dpred(m_test, f=f_full) d_mult = sum_sim.dpred(m_test, f=f_mult) - np.testing.assert_allclose(d_full, d_mult) + np.testing.assert_allclose(d_full, d_mult, rtol=1e-6) # test Jvec u = np.random.rand(mesh.n_cells) jvec_full = full_sim.Jvec(m_test, u, f=f_full) jvec_mult = sum_sim.Jvec(m_test, u, f=f_mult) - np.testing.assert_allclose(jvec_full, jvec_mult) + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) # test Jtvec v = np.random.rand(survey.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_mult) - np.testing.assert_allclose(jtvec_full, jtvec_mult) + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-6) # test get diag diag_full = full_sim.getJtJdiag(m_test, f=f_full) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index ac86d23e20..286e8ba6db 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,4 +1,5 @@ import unittest +import pytest import discretize from SimPEG import maps from SimPEG.potential_fields import gravity @@ -68,6 +69,11 @@ def get_block_inds(grid, block): sensitivity_path=str(tmp_path) + os.sep, ) + with pytest.raises(TypeError): + sim.sensitivity_dtype = float + + assert sim.sensitivity_dtype is np.float32 + data = sim.dpred(model_reduced) d_x = data[0::3] d_y = data[1::3] @@ -83,9 +89,10 @@ def get_block_inds(grid, block): + prism_2.gravitational_field(locXyz) + prism_3.gravitational_field(locXyz) ) * 1e5 # convert to mGal from m/s^2 - np.testing.assert_allclose(d_x, d[:, 0], rtol=1e-10, atol=1e-14) - np.testing.assert_allclose(d_y, d[:, 1], rtol=1e-10, atol=1e-14) - np.testing.assert_allclose(d_z, d[:, 2], rtol=1e-10, atol=1e-14) + d = d.astype(sim.sensitivity_dtype) + np.testing.assert_allclose(d_x, d[:, 0], rtol=1e-9, atol=1e-6) + np.testing.assert_allclose(d_y, d[:, 1], rtol=1e-9, atol=1e-6) + np.testing.assert_allclose(d_z, d[:, 2], rtol=1e-9, atol=1e-6) def test_ana_gg_forward(): @@ -151,6 +158,9 @@ def get_block_inds(grid, block): n_processes=None, ) + # forward only should default to np.float64 + assert sim.sensitivity_dtype is np.float64 + data = sim.dpred(model_reduced) d_xx = data[0::6] d_xy = data[1::6] From a6ba12f2b9156d34008440c55e4618b7caa65ef0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 18 Jul 2023 11:20:12 -0700 Subject: [PATCH 288/455] Fix sparse inversion example: remove beta schedule Modify example that runs a gravity sparse inversion: remove the `BetaSchedule` directive to avoid cooling beta twice. The `Update_IRLS` directive has its own builtin beta scheduling scheme. --- tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 1d79479e47..a58ae649dc 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -267,10 +267,6 @@ beta_tol=1e-2, ) -# Defining the fractional decrease in beta and the number of Gauss-Newton solves -# for each beta value. -beta_schedule = directives.BetaSchedule(coolingFactor=5, coolingRate=1) - # Options for outputting recovered models and predicted data for each beta. save_iteration = directives.SaveOutputEveryIteration(save_txt=False) @@ -285,7 +281,6 @@ update_IRLS, sensitivity_weights, starting_beta, - beta_schedule, save_iteration, update_jacobi, ] From 8aadd54207d1237af4b5ad2974a39ca19d53060e Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Wed, 19 Jul 2023 09:31:44 -0700 Subject: [PATCH 289/455] final unresolved comments (#1262) --- SimPEG/regularization/base.py | 6 +++--- SimPEG/regularization/pgi.py | 2 +- SimPEG/regularization/sparse.py | 20 ++++++++++++-------- SimPEG/regularization/vector.py | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index f5c0f151c3..1c89fb4ec4 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -19,7 +19,7 @@ class BaseRegularization(BaseObjectiveFunction): """Base regularization class. The ``BaseRegularization`` class defines properties and methods inherited by - SimPEG regularization classes. It is not directly used to constrain inversions. + SimPEG regularization classes, and is not directly used to construct inversions. Parameters ---------- @@ -174,12 +174,12 @@ def model(self, values: np.ndarray | float): @property def mapping(self) -> maps.IdentityMap: - """Mapping from the model to the regularization mesh. + """Mapping from the inversion model parameters to the regularization mesh. Returns ------- SimPEG.maps.BaseMap - The mapping from the model parameters to the quantity defined on the + The mapping from the inversion model parameters to the quantity defined on the :py:class:`~.regularization.RegularizationMesh`. """ return self._mapping diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 05f65433e8..ac6f405d63 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -293,7 +293,7 @@ def shape(self): Returns ------- - int + tuple of int Number of model parameters. """ return (self.wiresmap.nP,) diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index d81d19be1c..0ab1bb9a0b 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -69,8 +69,9 @@ def irls_scaled(self) -> bool: """Scale IRLS weights. When ``True``, scaling is applied when computing IRLS weights. - The scaling acts to preserve the balance between the data misfit and objective functions - in the regularization, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. For a comprehensive description, see the documentation for :py:meth:`get_lp_weights` . @@ -167,8 +168,9 @@ def get_lp_weights(self, f_m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and objective functions - in the regularization, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To apply elementwise scaling, let @@ -371,8 +373,9 @@ def update_weights(self, m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and objective functions - in the regularization, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To compute the scaling, let @@ -626,8 +629,9 @@ def update_weights(self, m): :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and objective functions - in the regularization, and improves convergence by ensuring the model does not deviate + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate aggressively from the global 2-norm solution during the first few IRLS iterations. To apply the scaling, let diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 96caf7591a..0dd37aad13 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -94,7 +94,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): .. math:: \phi (\vec{m}) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \cdot \, - \Big [ \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big ]^2 + \Big | \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big |^2 where :math:`\tilde{m}_i \in \mathbf{m}` are the model vectors at cell centers and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account From b80a5ba72ee59036836eb808417078c59d2da4db Mon Sep 17 00:00:00 2001 From: Andrea Balza Morales Date: Sun, 30 Jul 2023 11:32:47 -0600 Subject: [PATCH 290/455] Add building docs and serving them to documentation --- .../contributing/documentation.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index b9a6df7b91..504159377e 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -64,3 +64,22 @@ For example: If length scales are used to set the smoothness weights, alphas are respectively set internally using: >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 """ + + + +Building the documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you would like to see the documentation changes. +In the repo's root directory, enter the following in your terminal. + +.. code:: + make all + +Serving the documentation locally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. + +.. code:: + make serve From ed8a0ab3e7070de25eab66507e61dae57a4f370a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 30 Jul 2023 11:34:28 -0600 Subject: [PATCH 291/455] Add serve target to docs/Makefile Include a line in the help target --- docs/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 15a80d7bad..47d100db07 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,7 +14,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: all api help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext +.PHONY: all api help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext serve help: @echo "Please use \`make ' where is one of" @@ -38,6 +38,7 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " serve to serve the docs locally" clean: rm -rf $(BUILDDIR)/* @@ -166,3 +167,6 @@ doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." + +serve: + cd $(BUILDDIR)/html && python -m http.server 8001 From b9dbc489b918ef60e9970501058007531112a113 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 15:12:38 -0600 Subject: [PATCH 292/455] use setuptools_scm to track version --- .bumpversion.cfg | 4 ---- .gitignore | 3 +++ MANIFEST.in | 11 ++++++++++- SimPEG/__init__.py | 16 +++++++++++++++- docs/conf.py | 10 ++++------ environment_test.yml | 1 + requirements_dev.txt | 1 + setup.py | 11 ++++++++--- 8 files changed, 42 insertions(+), 15 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 5bcd931905..0000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[bumpversion] -current_version = 0.19.0 -files = setup.py SimPEG/__init__.py docs/conf.py - diff --git a/.gitignore b/.gitignore index 9418df3e91..f6cf59ad22 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ tutorials/13-joint_inversion/cross_gradient_data/* *.npy *.mod *.tar.gz + +# setuptools_scm +SimPEG/version.py diff --git a/MANIFEST.in b/MANIFEST.in index c74cefe094..7c04fa62b2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,10 @@ -include *.rst LICENSE +prune .github +prune .azure-pipelines +prune docs +prune examples +prune tutorials +prune tests +exclude .coveragerc .flake8 .gitignore MANIFEST.in .pre-commit-config.yaml +exclude azure-pipelines.yml .mailmap +exclude environment_test.yml +exclude requirements.txt requirements_dev.txt requirements_dask.txt requirements_style.txt \ No newline at end of file diff --git a/SimPEG/__init__.py b/SimPEG/__init__.py index 4f390f6886..8da0bc6f33 100644 --- a/SimPEG/__init__.py +++ b/SimPEG/__init__.py @@ -163,7 +163,21 @@ SolverBiCG, ) -__version__ = "0.19.0" __author__ = "SimPEG Team" __license__ = "MIT" __copyright__ = "2013 - 2020, SimPEG Team, http://simpeg.xyz" + + +# Version +try: + # - Released versions just tags: 0.8.0 + # - GitHub commits add .dev#+hash: 0.8.1.dev4+g2785721 + # - Uncommitted changes add timestamp: 0.8.1.dev4+g2785721.d20191022 + from SimPEG.version import version as __version__ +except ImportError: + # If it was not installed, then we don't know the version. We could throw a + # warning here, but this case *should* be rare. SimPEG should be + # installed properly! + from datetime import datetime + + __version__ = "unknown-" + datetime.today().strftime("%Y%m%d") diff --git a/docs/conf.py b/docs/conf.py index 69358c9ad4..23e2749fc0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,13 +13,11 @@ import sys import os -from datetime import datetime from sphinx_gallery.sorting import FileNameSortKey import glob import SimPEG import plotly.io as pio -import subprocess -import shutil +from importlib.metadata import version pio.renderers.default = "sphinx_gallery" @@ -81,10 +79,10 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = "0.19.0" # The full version, including alpha/beta/rc tags. -release = "0.19.0" +release = version("SimPEG") +# The short X.Y version. +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/environment_test.yml b/environment_test.yml index a63ace1bc9..b658b30427 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -10,6 +10,7 @@ dependencies: - discretize>=0.8 - geoana>=0.5.0 - empymod>=2.0.0 + - setuptools_scm - pandas - dask - zarr diff --git a/requirements_dev.txt b/requirements_dev.txt index a769ba3dfa..fdda338eac 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -24,3 +24,4 @@ pre-commit twine memory_profiler plotly +setuptools_scm diff --git a/setup.py b/setup.py index 675dcf6839..cf85b3a66e 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ parameter estimation in the context of geophysical applications. """ -from distutils.core import setup -from setuptools import find_packages +from setuptools import setup, find_packages +import os CLASSIFIERS = [ "Development Status :: 4 - Beta", @@ -29,9 +29,11 @@ setup( name="SimPEG", - version="0.19.0", packages=find_packages(exclude=["tests*", "examples*", "tutorials*"]), python_requires=">=3.8", + setup_requires=[ + "setuptools_scm", + ], install_requires=[ "numpy>=1.20", "scipy>=1.8", @@ -54,4 +56,7 @@ classifiers=CLASSIFIERS, platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], use_2to3=False, + use_scm_version={ + "write_to": os.path.join("SimPEG", "version.py"), + }, ) From 27a95577204fc520083811e8b8fa8dc09136d813 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 15:34:03 -0600 Subject: [PATCH 293/455] fix new flake errors --- SimPEG/directives/directives.py | 2 -- SimPEG/utils/code_utils.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index de38474488..2507b0e018 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -1643,8 +1643,6 @@ def endIter(self): class SaveOutputEveryIteration(SaveEveryIteration): """SaveOutputEveryIteration""" - save_txt = True - def __init__(self, save_txt=True, **kwargs): super().__init__(**kwargs) diff --git a/SimPEG/utils/code_utils.py b/SimPEG/utils/code_utils.py index 149d981e66..b0c5623a4c 100644 --- a/SimPEG/utils/code_utils.py +++ b/SimPEG/utils/code_utils.py @@ -1109,7 +1109,7 @@ def validate_type(property_name, obj, obj_type, cast=True, strict=False): f"{type(obj).__name__} cannot be converted to type {obj_type.__name__} " f"required for {property_name}." ) from err - if strict and type(obj) != obj_type: + if strict and type(obj) is not obj_type: raise TypeError( f"Object must be exactly a {obj_type.__name__} for {property_name}" ) From 9ac8aa49e708d33b24d8102a6de185169bfab671 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 16:48:07 -0600 Subject: [PATCH 294/455] update deploy script --- azure-pipelines.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6456e91c9b..2b6d0dcb57 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -92,8 +92,10 @@ stages: - script: | source "${HOME}/conda/etc/profile.d/conda.sh" source "${HOME}/conda/etc/profile.d/mamba.sh" - echo " - python="$(python.version) >> environment_test.yml - mamba env create -f environment_test.yml + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="$(python.version) >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines displayName: Create Anaconda testing environment From 0274c7a2be40fdc03879e6e298d8eac0a55162ce Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 18:11:11 -0600 Subject: [PATCH 295/455] Add release notes --- docs/content/release/0.20.0-notes.rst | 123 ++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/content/release/0.20.0-notes.rst diff --git a/docs/content/release/0.20.0-notes.rst b/docs/content/release/0.20.0-notes.rst new file mode 100644 index 0000000000..8b84576942 --- /dev/null +++ b/docs/content/release/0.20.0-notes.rst @@ -0,0 +1,123 @@ +.. _0.20.0_notes: + +=========================== +SimPEG 0.20.0 Release Notes +=========================== + +August 9th, 2023 + +This minor release contains many bugfixes and additions to the code base, including improvements to +documentation for regularization. + +.. contents:: Highlights + :depth: 2 + + +Updates +======= + +Spontaneous Potential +--------------------- +The spontaneous (self) potential module has finally been re-implemented into the +simulation framework of simpeg 0.14.0. Check out the new module at +:py:mod:`simpeg.electromagnetics.static.spontaneous_potential`. + +MVI inversions +-------------- +There are now two new support regularization functions relevant to vector inversions in +the cartesian domain. an amplitude based regularization, and a direction based regularization, +which both support reference models. + +FDEM +---- +The frequency domain simulations now support forward modeling with electrical permittivity as a property + +Flake8 improvements +------------------- +We continue to add improvements to the internal code structures to be more in line with +flake8 practices. + +MetaSimulation +-------------- +There is now a multiprocessing version of the `MetaSimulation` class for interested users +to experiment with. + +Documentation +------------- +We've made substantial additions to the regularization documentation. + +We have also updated the getting started guides to represent current recommend +practices for installing, developing with, and contributing to SimPEG. + +Others +------ +We've added support for taking derivatives of anisotropic models, which also fixed derivatives of +properties on tetrahedral and curvilinear meshes. + +Invertible properties may now be not required for certain simulations (e.g. permittivity in +a FDEM simulation). + +Last but not least, there are of course many bugfixes! + +Contributors +============ +This is a combination of contributors and reviewers who've made contributions towards +this release (in no particular order). + +* `@jcapriot `__ +* `@santisoler `__ +* `@domfournier `__ +* `@dccowan `__ +* `@thibaut-kobold `__ +* `@nwilliams-kobold `__ +* `@lheagy `__ +* `@yanang007* `__ +* `@andieie* `__ + +Pull requests +============= +* `#1103 `__: Amplitude regularization +* `#1195 `__: Refactor PGI_BetaAlphaSchedule directive +* `#1201 `__: Multiprocessing MetaSimulation +* `#1211 `__: Sp reimplement +* `#1212 `__: Add a linearity property to mappings +* `#1213 `__: Pydata sphinx theme updates +* `#1214 `__: Cross reference vector +* `#1215 `__: Meta/meta patches +* `#1216 `__: Tiny typo triggers error when displaying error output string +* `#1217 `__: Update index.rst +* `#1224 `__: Replace deprecated numpy type aliases with builtin types +* `#1225 `__: Regularization docstrings +* `#1229 `__: Generalize __add__ for any ComboObjectiveFunction +* `#1230 `__: Discretize 0.9.0updates +* `#1231 `__: Fix IP simulation / inversion with SimPEG.dask +* `#1234 `__: General Doc cleanup +* `#1235 `__: conditionally allow invertible property to also be optional +* `#1236 `__: FDEM permittivity +* `#1237 `__: Anisotropy derivative support +* `#1238 `__: Move flake8 ignored rules to `.flake8` and rename Makefile targets +* `#1239 `__: Add flake8 to pre-commit configuration +* `#1240 `__: Merge docs for developers into a Contributing section +* `#1241 `__: Refactor `BaseObjectiveFunction` and `ComboObjectiveFunction` +* `#1242 `__: Fix flake `E711` error: wrong comparison with None +* `#1243 `__: Fix flake `E731` error: assign lambda functions +* `#1244 `__: Fix flake `F403` and `F405` errors: don't use star imports +* `#1245 `__: Fix `F522`, `F523`, `F524` flake errors: format calls +* `#1246 `__: Fix `F541` flake error: f-string without placeholder +* `#1247 `__: Simplify CONTRIBUTING.md +* `#1248 `__: Fix F811 flake error: remove redefinitions +* `#1249 `__: Add more hints about pre-commit in documentation +* `#1250 `__: Rename "basic" directory in docs to "getting_started" +* `#1251 `__: Test patches +* `#1252 `__: Fix W291 and W293 flake errors: white spaces +* `#1253 `__: Always calculate gzz if needed +* `#1254 `__: Fix B028 flake error: non-explicit stacklevel +* `#1256 `__: Make units of gravity simulations more explicit +* `#1257 `__: unpack the data misfits for plotting tikhonov curves +* `#1258 `__: Update pull_request_template.md +* `#1260 `__: Optionally import utm +* `#1261 `__: Set storage type of pf sensitivity matrix +* `#1262 `__: final unresolved comments for PR #1225 +* `#1264 `__: Fix sparse inversion example: remove beta schedule +* `#1267 `__: Add building docs and serving them to documentation +* `#1274 `__: use setuptools_scm to track version From eaa77f7f480151d83ecb7be061cbe24cdc2557d1 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 18:16:30 -0600 Subject: [PATCH 296/455] add future warnings from 0.19.0 --- SimPEG/directives/directives.py | 4 +++- SimPEG/flow/richards/simulation.py | 6 +++++- SimPEG/utils/io_utils/io_utils_pf.py | 8 ++++---- SimPEG/utils/mat_utils.py | 8 ++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 2507b0e018..6700257d9a 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -89,7 +89,9 @@ def verbose(self): def verbose(self, value): self._verbose = validate_type("verbose", value, bool) - debug = deprecate_property(verbose, "debug", "verbose", removal_version="0.19.0") + debug = deprecate_property( + verbose, "debug", "verbose", removal_version="0.19.0", future_warn=True + ) @property def inversion(self): diff --git a/SimPEG/flow/richards/simulation.py b/SimPEG/flow/richards/simulation.py index 81452dd22b..b897230230 100644 --- a/SimPEG/flow/richards/simulation.py +++ b/SimPEG/flow/richards/simulation.py @@ -87,7 +87,11 @@ def initial_conditions(self, value): ) debug = deprecate_property( - BaseTimeSimulation.verbose, "debug", "verbose", removal_version="0.19.0" + BaseTimeSimulation.verbose, + "debug", + "verbose", + removal_version="0.19.0", + future_warn=True, ) @property diff --git a/SimPEG/utils/io_utils/io_utils_pf.py b/SimPEG/utils/io_utils/io_utils_pf.py index ec31d8ba5c..b5048d908a 100644 --- a/SimPEG/utils/io_utils/io_utils_pf.py +++ b/SimPEG/utils/io_utils/io_utils_pf.py @@ -390,23 +390,23 @@ def write_gg3d_ubc(filename, data_object): read_mag3d_ubc, "readUBCmagneticsObservations", removal_version="0.14.4", - future_warn=True, + error=True, ) writeUBCmagneticsObservations = deprecate_method( write_mag3d_ubc, "writeUBCmagneticsObservations", removal_version="0.14.4", - future_warn=True, + error=True, ) readUBCgravityObservations = deprecate_method( read_grav3d_ubc, "readUBCgravityObservations", removal_version="0.14.4", - future_warn=True, + error=True, ) writeUBCgravityObservations = deprecate_method( write_grav3d_ubc, "writeUBCgravityObservations", removal_version="0.14.4", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index a856c6581e..0c159aea26 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -455,8 +455,12 @@ def define_plane_from_points(xyz1, xyz2, xyz3): ################################################ -diagEst = deprecate_function(estimate_diagonal, "diagEst", removal_version="0.19.0") -uniqueRows = deprecate_function(unique_rows, "uniqueRows", removal_version="0.19.0") +diagEst = deprecate_function( + estimate_diagonal, "diagEst", removal_version="0.19.0", future_warn=True +) +uniqueRows = deprecate_function( + unique_rows, "uniqueRows", removal_version="0.19.0", future_warn=True +) sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", future_warn=True) getSubArray = deprecate_function( get_subarray, "getSubArray", removal_version="0.19.0", future_warn=True From 3b3dc4064573ef0a49758d2bde56a69b859d48b2 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 8 Aug 2023 18:18:07 -0600 Subject: [PATCH 297/455] add self referencing PR --- docs/content/release/0.20.0-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/release/0.20.0-notes.rst b/docs/content/release/0.20.0-notes.rst index 8b84576942..7eab92d463 100644 --- a/docs/content/release/0.20.0-notes.rst +++ b/docs/content/release/0.20.0-notes.rst @@ -121,3 +121,4 @@ Pull requests * `#1264 `__: Fix sparse inversion example: remove beta schedule * `#1267 `__: Add building docs and serving them to documentation * `#1274 `__: use setuptools_scm to track version +* `#1275 `__: 0.20.0 staging \ No newline at end of file From 08c04a483289f9bb42595352a5e22f2c84dff57b Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 9 Aug 2023 08:40:53 -0600 Subject: [PATCH 298/455] Import BaseAmplitude for docs --- SimPEG/regularization/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 25625de08b..5d1a7910ac 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -164,6 +164,7 @@ from .vector import ( BaseVectorRegularization, CrossReferenceRegularization, + BaseAmplitude, VectorAmplitude, AmplitudeSmallness, AmplitudeSmoothnessFirstOrder, From a1e8984b80c95aa5d174acf5f0aa36e66ac85ab6 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 9 Aug 2023 08:42:52 -0600 Subject: [PATCH 299/455] fix documentation rst codeblock --- docs/content/getting_started/contributing/documentation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index 504159377e..835832273f 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -73,7 +73,8 @@ Building the documentation If you would like to see the documentation changes. In the repo's root directory, enter the following in your terminal. -.. code:: +.. code:: + make all Serving the documentation locally @@ -82,4 +83,5 @@ Serving the documentation locally Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. .. code:: + make serve From a49411f5a652a0aee5e7a8b022489df534d07af4 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 9 Aug 2023 09:59:15 -0600 Subject: [PATCH 300/455] ignore BaseAmplitude in generalized regularization tests. --- tests/base/test_regularization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index b03376a0ab..ceb910baf2 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -30,6 +30,7 @@ "CrossGradient", "LinearCorrespondence", "JointTotalVariation", + "BaseAmplitude", "VectorAmplitude", "CrossReferenceRegularization", ] From d41482068345a117c564a7a4385945fced7ad41d Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 9 Aug 2023 16:52:57 -0600 Subject: [PATCH 301/455] Add 0.20.0 release notes to toc --- docs/content/release/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index dd81c94be7..98e30d6b57 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.20.0 <0.20.0-notes> 0.19.0 <0.19.0-notes> 0.18.1 <0.18.1-notes> 0.18.0 <0.18.0-notes> From 172041e5a31659d0154d5b2f2d6b1b2911eb5d6f Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 23 Aug 2023 12:37:08 -0700 Subject: [PATCH 302/455] add plausible analytics to simpeg docs --- docs/_templates/layout.html | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index a06350d4c1..f208c5eced 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -9,14 +9,5 @@ - + {% endblock %} From 95aed99402aab2378b6440cf735bcb93bce5540f Mon Sep 17 00:00:00 2001 From: Matthew Plough Date: Wed, 6 Sep 2023 10:04:12 -0400 Subject: [PATCH 303/455] Update links in documentation --- README.rst | 6 +++--- SimPEG/__init__.py | 2 +- SimPEG/electromagnetics/analytics/NSEM.py | 2 +- SimPEG/utils/mat_utils.py | 2 +- docs/conf.py | 6 +++--- .../getting_started/contributing/advanced.rst | 6 +++--- .../getting_started/contributing/documentation.rst | 2 +- .../getting_started/contributing/testing.rst | 2 +- .../contributing/working-with-github.rst | 8 ++++---- docs/content/getting_started/installing.rst | 14 +++++++------- .../20-published/plot_schenkel_morrison_casing.py | 2 +- setup.py | 4 ++-- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 2f3c023ce7..bb8122935b 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ SimPEG :target: https://doi.org/10.5281/zenodo.596373 .. image:: https://img.shields.io/discourse/users?server=http%3A%2F%2Fsimpeg.discourse.group%2F - :target: http://simpeg.discourse.group/ + :target: https://simpeg.discourse.group/ .. image:: https://img.shields.io/badge/Slack-simpeg-4A154B.svg?logo=slack :target: http://slack.simpeg.xyz @@ -46,7 +46,7 @@ The vision is to create a package for finite volume simulation with applications * supports 1D, 2D and 3D problems * designed for large-scale inversions -You are welcome to join our forum and engage with people who use and develop SimPEG at: http://simpeg.discourse.group/. +You are welcome to join our forum and engage with people who use and develop SimPEG at: https://simpeg.discourse.group/. Weekly meetings are open to all. They are generally held on Wednesdays at 10:30am PDT. Please see the calendar (`GCAL `_, `ICAL `_) for information on the next meeting. @@ -159,5 +159,5 @@ Contributing We always welcome contributions towards SimPEG whether they are adding new code, suggesting improvements to existing codes, identifying bugs, providing examples, or anything that will improve SimPEG. -Please checkout the `contributing guide `_ +Please checkout the `contributing guide `_ for more information on how to contribute. diff --git a/SimPEG/__init__.py b/SimPEG/__init__.py index 8da0bc6f33..18cf134a19 100644 --- a/SimPEG/__init__.py +++ b/SimPEG/__init__.py @@ -165,7 +165,7 @@ __author__ = "SimPEG Team" __license__ = "MIT" -__copyright__ = "2013 - 2020, SimPEG Team, http://simpeg.xyz" +__copyright__ = "2013 - 2020, SimPEG Team, https://simpeg.xyz" # Version diff --git a/SimPEG/electromagnetics/analytics/NSEM.py b/SimPEG/electromagnetics/analytics/NSEM.py index b9c3dffc88..3df2612320 100644 --- a/SimPEG/electromagnetics/analytics/NSEM.py +++ b/SimPEG/electromagnetics/analytics/NSEM.py @@ -103,7 +103,7 @@ def MT_LayeredEarth( This code compute the analytic response of a n-layered Earth to a plane wave (Magnetotellurics). All physical properties arrays convention describes the layers parameters from the top layer to the bottom layer. The solution is first developed in Ward and Hohmann 1988. - See also http://em.geosci.xyz/content/maxwell3_fdem/natural_sources/MT_N_layered_Earth.html + See also https://em.geosci.xyz/content/maxwell3_fdem/natural_sources/MT_N_layered_Earth.html :param freq: the frequency at which we take the measurements :type freq: float or numpy.ndarray diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index 0c159aea26..cbf9221d3f 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -39,7 +39,7 @@ def estimate_diagonal(matrix_arg, n, k=None, approach="Probing"): and a vector. For background information on this method, see - `Bekas (et al., 2005) `__ + `Bekas (et al., 2005) `__ and `Selig (et al., 2012) `__ Parameters diff --git a/docs/conf.py b/docs/conf.py index 23e2749fc0..cea63da1c6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,7 @@ # General information about the project. project = "SimPEG" -copyright = "2013 - 2023, SimPEG Team, http://simpeg.xyz" +copyright = "2013 - 2023, SimPEG Team, https://simpeg.xyz" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -418,9 +418,9 @@ def linkcode_resolve(domain, info): "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), - "matplotlib": ("http://matplotlib.org/stable/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), "properties": ("https://propertiespy.readthedocs.io/en/latest/", None), - "discretize": ("http://discretize.simpeg.xyz/en/main/", None), + "discretize": ("https://discretize.simpeg.xyz/en/main/", None), } numpydoc_xref_param_type = True diff --git a/docs/content/getting_started/contributing/advanced.rst b/docs/content/getting_started/contributing/advanced.rst index eb4a01bf90..655a0e4dca 100644 --- a/docs/content/getting_started/contributing/advanced.rst +++ b/docs/content/getting_started/contributing/advanced.rst @@ -10,7 +10,7 @@ follow the instructions to download and install pymatsolver_. .. _Pardiso: https://www.pardiso-project.org -.. _pymatsolver: https://github.com/rowanc1/pymatsolver +.. _pymatsolver: https://github.com/simpeg/pymatsolver If you open a `Jupyter notebook`_ and are able to run: @@ -18,8 +18,8 @@ If you open a `Jupyter notebook`_ and are able to run: from pymatsolver import Pardiso -.. _Jupyter notebook: http://jupyter.org/ +.. _Jupyter notebook: https://jupyter.org/ then you have succeeded! Otherwise, make an `issue in pymatsolver`_. -.. _issue in pymatsolver: https://github.com/rowanc1/pymatsolver/issues +.. _issue in pymatsolver: https://github.com/simpeg/pymatsolver/issues diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index 835832273f..d4bda313e0 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -6,7 +6,7 @@ Documentation Documentation helps others use your code! Please document new contributions. SimPEG tries to follow the `numpydoc` style of docstrings (check out the `style guide `_). -SimPEG then uses `sphinx `_ to build the documentation. +SimPEG then uses `sphinx `_ to build the documentation. When documenting a new class or function, please include a description (with math if it solves an equation), inputs, outputs and preferably a small example. diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/getting_started/contributing/testing.rst index ba8de39983..ba4e207e86 100644 --- a/docs/content/getting_started/contributing/testing.rst +++ b/docs/content/getting_started/contributing/testing.rst @@ -13,7 +13,7 @@ Testing On each update, SimPEG is tested using the continuous integration service `Azure pipelines `_. -We use `Codecov `_ to check and provide stats on how much +We use `Codecov `_ to check and provide stats on how much of the code base is covered by tests. This tells which lines of code have been run in the test suite. It does not tell you about the quality of the tests run! In order to assess that, have a look at the tests we are running - they tell you diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/getting_started/contributing/working-with-github.rst index e7a05ddbc6..2876c1e0c2 100644 --- a/docs/content/getting_started/contributing/working-with-github.rst +++ b/docs/content/getting_started/contributing/working-with-github.rst @@ -6,12 +6,12 @@ Working with Git and GitHub .. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png :align: right :width: 100 - :target: https://github.com + :target: https://github.com To keep track of your code changes and contribute back to SimPEG, you will need a Github_ account. Then fork the `SimPEG repository -`_ to your local account. +`_ to your local account. (`How to fork a repo `_). @@ -40,9 +40,9 @@ There are two ways you can clone a repository: For managing your copy of SimPEG and contributing back to the main repository, have a look at the article: `A successful git branching model -`_ +`_ -.. _Github: https://github.com +.. _Github: https://github.com .. _SourceTree: https://www.sourcetreeapp.com/ .. _GitKraken: https://www.gitkraken.com/ diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst index 61eb1866da..7105415ad6 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -12,10 +12,10 @@ Prerequisite: Installing Python SimPEG is written in Python_! We highly recommend installing it using Anaconda_ (or the alternative Mambaforge_). It installs `Python `_, -`Jupyter `_ and other core +`Jupyter `_ and other core Python libraries for scientific computing. If you and Python_ are not yet acquainted, we highly -recommend checking out `Software Carpentry `_. +recommend checking out `Software Carpentry `_. .. note:: @@ -100,7 +100,7 @@ Success? ======== If you have been successful at downloading and installing SimPEG, you should -be able to download and run any of the :ref:`examples and tutorials`. +be able to download and run any of the :ref:`examples and tutorials `. If not, you can reach out to other people developing and using SimPEG on the `google forum `_ or on @@ -120,8 +120,8 @@ Python for scientific computing ------------------------------- * `Python for Scientists `_ Links to commonly used packages, Matlab to Python comparison -* `Python Wiki `_ Lists packages and resources for scientific computing in Python -* `Jupyter `_ +* `Python Wiki `_ Lists packages and resources for scientific computing in Python +* `Jupyter `_ Numpy and Matlab ---------------- @@ -140,9 +140,9 @@ Editing Python -------------- There are numerous ways to edit and test Python (see -`PythonWiki `_ for an overview) and +`PythonWiki `_ for an overview) and in our group at least the following options are being used: -* `Jupyter `_ +* `Jupyter `_ * `Sublime `_ * `PyCharm `_ diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index 2511751138..4868459e2f 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -35,7 +35,7 @@ and achieve the CPU time of ~30s is Mumps, which was installed using pymatsolver_ -.. _pymatsolver: https://github.com/rowanc1/pymatsolver +.. _pymatsolver: https://github.com/simpeg/pymatsolver This example is on figshare: https://dx.doi.org/10.6084/m9.figshare.3126961.v1 diff --git a/setup.py b/setup.py index cf85b3a66e..1a2459028e 100644 --- a/setup.py +++ b/setup.py @@ -51,8 +51,8 @@ long_description=LONG_DESCRIPTION, license="MIT", keywords="geophysics inverse problem", - url="http://simpeg.xyz/", - download_url="https://github.com/simpeg/simpeg", + url="https://simpeg.xyz/", + download_url="https://github.com/simpeg/simpeg", classifiers=CLASSIFIERS, platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], use_2to3=False, From e1667dbe5bff50ac5cc13f22c5fc8610ec25c421 Mon Sep 17 00:00:00 2001 From: Matthew Plough Date: Wed, 6 Sep 2023 10:07:50 -0400 Subject: [PATCH 304/455] Update links in .github --- .github/ISSUE_TEMPLATE/feature-request.yml | 4 ++-- .github/pull_request_template.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index c42bef31e8..c7db801f1a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -11,11 +11,11 @@ body: developers on [slack](https://slack.simpeg.xyz), in addition to opening an issue or pull request here. You can also check out our - [Contributor Guide](https://docs.simpeg.xyz/content/getting_started/contributing.html) + [Contributor Guide](https://docs.simpeg.xyz/content/getting_started/contributing/index.html) if you need more information. - type: textarea attributes: label: "Proposed new feature or change:" validations: - required: true \ No newline at end of file + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 06d6b74691..c6b74e7c19 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,7 @@ Thanks for contributing a pull request to SimPEG! Remember to use a personal fork of SimPEG to propose changes. Check out the stages of a pull request at -https://docs.simpeg.xyz/content/getting_started/contributing.html#pull-request +https://docs.simpeg.xyz/content/getting_started/contributing/pull-requests.html Note that we are a team of volunteers and we appreciate your patience during the review process. @@ -18,7 +18,7 @@ Feel free to remove lines from this template that do not apply to you pull reque #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR -* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/practices.html#style). +* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). * [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). From 875fc3239b9060e9f124a9be36708e0fa1b3dc4a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 21 Sep 2023 09:48:43 -0700 Subject: [PATCH 305/455] Run pytest on Azure with increased verbosity (#1287) Run `pytest` with the `-v` option in Azure Pipelines. In some situations might be good to know precisely which tests functions are being run and with which combination of parameters in Azure Pipelines. --- .azure-pipelines/matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 8f5afb8cb6..fd6cc259a3 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -47,7 +47,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test export KMP_WARNINGS=0 - pytest ${{ test }} --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning + pytest ${{ test }} -v --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning displayName: 'Testing ${{ test }}' - script: | From c477bbb2bb5af67b0ea5ece698ee74250486a7e0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 27 Sep 2023 10:21:17 -0700 Subject: [PATCH 306/455] Allow to use random seed in make_synthetic_data (#1286) Add a new random_seed argument to the make_synthetic_data method. Switch to the Numpy's random number generator object and pass the random_seed argument to np.random.default_rng, allowing users to specify a random seed for the synthetic noise. Set random seeds in some tests that were failing due to the new random state that the usage of make_synthetic_data with random_seed created in the test suite. --- SimPEG/simulation.py | 35 +++++++++++++++---- tests/base/test_maps.py | 2 +- tests/em/static/test_DC_2D_jvecjtvecadj.py | 8 ++--- tests/meta/test_meta_sim.py | 18 ++++++---- tests/pf/test_grav_inversion_linear.py | 6 +++- tests/pf/test_mag_inversion_linear.py | 6 +++- tests/pf/test_pf_quadtree_inversion_linear.py | 16 ++++----- 7 files changed, 60 insertions(+), 31 deletions(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index bd45647c62..7e88483b98 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -305,14 +305,36 @@ def residual(self, m, dobs, f=None): return mkvc(self.dpred(m, f=f) - dobs) def make_synthetic_data( - self, m, relative_error=0.05, noise_floor=0.0, f=None, add_noise=False, **kwargs + self, + m, + relative_error=0.05, + noise_floor=0.0, + f=None, + add_noise=False, + random_seed=None, + **kwargs, ): """ Make synthetic data given a model, and a standard deviation. - :param numpy.ndarray m: geophysical model - :param numpy.ndarray | float relative_error: standard deviation - :param numpy.ndarray | float noise_floor: noise floor - :param numpy.ndarray f: fields for the given model (if pre-calculated) + + Parameters + ---------- + m : array + Array containing with geophysical model. + relative_error : float + Standard deviation. + noise_floor : float + Noise floor. + f : array or None + Fields for the given model (if pre-calculated). + add_noise : bool + Whether to add gaussian noise to the synthetic data or not. + random_seed : int or None + Random seed to pass to `numpy.random.default_rng`. + + Returns + ------- + SyntheticData """ std = kwargs.pop("std", None) @@ -328,7 +350,8 @@ def make_synthetic_data( if add_noise is True: std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) - noise = std * np.random.randn(*dclean.shape) + random_num_generator = np.random.default_rng(seed=random_seed) + noise = random_num_generator.normal(loc=0, scale=std, size=dclean.shape) dobs = dclean + noise else: dobs = dclean diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index f037b2d5f4..9f6c8aaec3 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -589,7 +589,7 @@ class TestSCEMT(unittest.TestCase): def test_sphericalInclusions(self): mesh = discretize.TensorMesh([4, 5, 3]) mapping = maps.SelfConsistentEffectiveMedium(mesh, sigma0=1e-1, sigma1=1.0) - m = np.abs(np.random.rand(mesh.nC)) + m = np.random.default_rng(seed=0).random(mesh.n_cells) mapping.test(m=m, dx=0.05 * np.ones(mesh.n_cells), num=3) def test_spheroidalInclusions(self): diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index a0fb2d18fd..0dcbb887db 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -18,8 +18,6 @@ except ImportError: from SimPEG import SolverLU as Solver -np.random.seed(41) - class DCProblem_2DTests(unittest.TestCase): formulation = "Simulation2DCellCentered" @@ -85,9 +83,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - # u = np.random.rand(self.mesh.nC * self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.data.nD) + rng = np.random.default_rng(seed=41) + v = rng.random(self.mesh.nC) + w = rng.random(self.data.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < self.adjoint_tol diff --git a/tests/meta/test_meta_sim.py b/tests/meta/test_meta_sim.py index c0a0ccf660..2530efcf7d 100644 --- a/tests/meta/test_meta_sim.py +++ b/tests/meta/test_meta_sim.py @@ -61,14 +61,15 @@ def test_multi_sim_correctness(): np.testing.assert_allclose(d_full, d_mult) # test Jvec - u = np.random.rand(mesh.n_cells) + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) jvec_full = full_sim.Jvec(m_test, u, f=f_full) jvec_mult = multi_sim.Jvec(m_test, u, f=f_mult) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(survey_full.nD) + v = rng.random(survey_full.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = multi_sim.Jtvec(m_test, v, f=f_mult) @@ -141,14 +142,16 @@ def test_sum_sim_correctness(): np.testing.assert_allclose(d_full, d_mult, rtol=1e-6) # test Jvec - u = np.random.rand(mesh.n_cells) + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) jvec_full = full_sim.Jvec(m_test, u, f=f_full) jvec_mult = sum_sim.Jvec(m_test, u, f=f_mult) np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) # test Jtvec - v = np.random.rand(survey.nD) + rng = np.random.default_rng(seed=0) + v = rng.random(survey.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_mult) @@ -218,7 +221,8 @@ def test_repeat_sim_correctness(): multi_sim = MetaSimulation(simulations, mappings) repeat_sim = RepeatedSimulation(sim, mappings) - model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) + rng = np.random.default_rng(seed=0) + model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) # test field things f_full = multi_sim.fields(model) @@ -230,13 +234,13 @@ def test_repeat_sim_correctness(): np.testing.assert_equal(d_full, d_repeat) # test Jvec - u = np.random.rand(len(model)) + u = rng.random(len(model)) jvec_full = multi_sim.Jvec(model, u, f=f_full) jvec_mult = repeat_sim.Jvec(model, u, f=f_mult) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(len(sim_ts) * survey.nD) + v = rng.random(len(sim_ts) * survey.nD) jtvec_full = multi_sim.Jtvec(model, v, f=f_full) jtvec_mult = repeat_sim.Jtvec(model, v, f=f_mult) np.testing.assert_allclose(jtvec_full, jtvec_mult) diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 733cec9bfe..4c98e6aecc 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -84,7 +84,11 @@ def setUp(self): # Compute linear forward operator and compute some data data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=0.0005, add_noise=True + self.model, + relative_error=0.0, + noise_floor=0.0005, + add_noise=True, + random_seed=2, ) # Create a regularization diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index ff8adfdfd7..868ad8b34b 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -88,7 +88,11 @@ def setUp(self): # Compute linear forward operator and compute some data data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=2, ) # Create a regularization diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 88f76433e5..e498dfd8a7 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -17,8 +17,6 @@ ) from SimPEG.potential_fields import gravity, magnetics -np.random.seed(44) - class QuadTreeLinProblemTest(unittest.TestCase): def setUp(self): @@ -101,6 +99,7 @@ def create_gravity_sim_flat(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=44, ) def create_magnetics_sim_flat(self, block_value=1.0, noise_floor=0.01): @@ -128,6 +127,7 @@ def create_magnetics_sim_flat(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=44, ) def create_gravity_sim(self, block_value=1.0, noise_floor=0.01): @@ -154,6 +154,7 @@ def create_gravity_sim(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_magnetics_sim(self, block_value=1.0, noise_floor=0.01): @@ -181,6 +182,7 @@ def create_magnetics_sim(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): @@ -208,6 +210,7 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): @@ -236,6 +239,7 @@ def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_inversion(self, sim, data, beta=1e3, all_active=True): @@ -445,8 +449,6 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): print("Z_TOP OR Z_BOTTOM LENGTH MATCHING NACTIVE-CELLS ERROR TEST PASSED.") def test_quadtree_grav_inverse(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.grav_inv.run(np.zeros(self.mesh.nC)) @@ -465,8 +467,6 @@ def test_quadtree_grav_inverse(self): self.assertLess(data_misfit, dpred.shape[0] * 1.15) def test_quadtree_mag_inverse(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.mag_inv.run(np.zeros(self.mesh.nC)) @@ -485,8 +485,6 @@ def test_quadtree_mag_inverse(self): self.assertLess(data_misfit, dpred.shape[0] * 1.1) def test_quadtree_grav_inverse_activecells(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.grav_inv_active.run(np.zeros(int(self.active_cells.sum()))) @@ -509,8 +507,6 @@ def test_quadtree_grav_inverse_activecells(self): self.assertLess(data_misfit, dpred.shape[0] * 1.1) def test_quadtree_mag_inverse_activecells(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.mag_inv_active.run(np.zeros(int(self.active_cells.sum()))) From dd0d467f75f5d9e7c59f66d55d5df43cf513cd77 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 10 Oct 2023 15:34:41 -0700 Subject: [PATCH 307/455] pgi doc --- SimPEG/regularization/pgi.py | 52 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index ac6f405d63..b3dd12d06d 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -94,14 +94,20 @@ class PGIsmallness(Smallness): (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) defining the GMM. And let :math:`\mathbf{z}^\ast` define an membership array that extracts the GMM parameters for the most representative rock unit within each active cell - in the :class:`RegularizationMesh`. The regularization function (objective function) for - ``PGIsmallness`` is given by: + in the :class:`RegularizationMesh`. + + When the ``approx_eval`` property is ``True``, we assume the physical property mean values of each geologic units + are distinct (no significant overlap of their respective physical properties distribution). The GMM probability + density value at any each point of the physical property space can then be approximated by the locally dominant + Gaussian distribution. In this case, the PGI regularization function (objective function) can be expressed as a + least-square: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} - \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ]^T - \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, - \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] + \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} + \big | \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, (\mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \, \Big \|^2 + &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where @@ -109,6 +115,14 @@ class PGIsmallness(Smallness): - :math:`\mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast )` is the reference model, and - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is a weighting matrix. + For the full, non-approximated PGI regularization, please refer to + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` and ``approx_hessian`` + properties to ``True`` so that the least-squares approximation is used to compute the gradient, as it is making the + same assumptions about the GMM. + ``PGIsmallness`` regularization can be used for models consisting of one or more physical property types. The ordering of the physical property types within the model is defined using the `wiresmap`. And the mapping from model parameter values to physical property @@ -118,17 +132,6 @@ class PGIsmallness(Smallness): .. math:: \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} - When the ``approx_eval`` property is ``True``, we assume the physical property types have - values that are uncorrelated. In this case, the weighting matrix is diagonal and the - regularization function (objective function) can be expressed as: - - .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, - \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 - - When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` property - to ``True`` so that the least-squares approximation is used to compute the gradient. - **Constructing the Reference Model and Weighting Matrix:** The reference model used in the regularization function is constructed by extracting the means @@ -142,19 +145,8 @@ class PGIsmallness(Smallness): :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: .. math:: - \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, - diag \big ( \mathbf{v \odot w} \big ) - - where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` - are custom cell weights. When the ``approx_eval`` property is ``True``, the off-diagonal - covariances are zero and we can use a weighting matrix of the form: - - .. math:: - \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} - \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) - - where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the - membership array :math:`\mathbf{z}^\ast`. + \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{\frac{-1}{2}} \, + diag \big ( \mathbf{w} \big ) **Updating the Gaussian Mixture Model:** From baf4099aba14ce64dd3ba1b08afb1e7d59801bd7 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 10 Oct 2023 20:15:52 -0700 Subject: [PATCH 308/455] typo --- SimPEG/regularization/pgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index b3dd12d06d..496df06f45 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -96,7 +96,7 @@ class PGIsmallness(Smallness): extracts the GMM parameters for the most representative rock unit within each active cell in the :class:`RegularizationMesh`. - When the ``approx_eval`` property is ``True``, we assume the physical property mean values of each geologic units + When the ``approx_eval`` property is ``True``, we assume the physical property distributions of each geologic units are distinct (no significant overlap of their respective physical properties distribution). The GMM probability density value at any each point of the physical property space can then be approximated by the locally dominant Gaussian distribution. In this case, the PGI regularization function (objective function) can be expressed as a From 1a910fff2d257b14cdd517d80dfa2256ccfae9a6 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 8 Nov 2023 08:59:14 -0800 Subject: [PATCH 309/455] Use max of rows of cell_gradient --- SimPEG/regularization/regularization_mesh.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index ae02a73eca..cff792b941 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -551,8 +551,8 @@ def cell_distances_x(self) -> np.ndarray: Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: - Ave = self.aveCC2Fx - self._cell_distances_x = Ave * (self.Pac.T * self.mesh.h_gridded[:, 0]) + self._cell_distances_x = np.max(self.cell_gradient_x, axis=1).data ** (-1.0) + return self._cell_distances_x @property @@ -565,8 +565,8 @@ def cell_distances_y(self) -> np.ndarray: Cell center distance array along the y-direction. """ if getattr(self, "_cell_distances_y", None) is None: - Ave = self.aveCC2Fy - self._cell_distances_y = Ave * (self.Pac.T * self.mesh.h_gridded[:, 1]) + self._cell_distances_y = np.max(self.cell_gradient_y, axis=1).data ** (-1.0) + return self._cell_distances_y @property @@ -579,8 +579,8 @@ def cell_distances_z(self) -> np.ndarray: Cell center distance array along the z-direction. """ if getattr(self, "_cell_distances_z", None) is None: - Ave = self.aveCC2Fz - self._cell_distances_z = Ave * (self.Pac.T * self.mesh.h_gridded[:, 2]) + self._cell_distances_z = np.max(self.cell_gradient_z, axis=1).data ** (-1.0) + return self._cell_distances_z From 0388c08fe793493c1eaf639a425cf31745aab5dc Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 8 Nov 2023 09:40:18 -0800 Subject: [PATCH 310/455] Switch to sparse max --- SimPEG/regularization/regularization_mesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index cff792b941..04cdb0c66c 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -551,7 +551,7 @@ def cell_distances_x(self) -> np.ndarray: Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: - self._cell_distances_x = np.max(self.cell_gradient_x, axis=1).data ** (-1.0) + self._cell_distances_x = self.cell_gradient_x.max(axis=1).data ** (-1.0) return self._cell_distances_x @@ -565,7 +565,7 @@ def cell_distances_y(self) -> np.ndarray: Cell center distance array along the y-direction. """ if getattr(self, "_cell_distances_y", None) is None: - self._cell_distances_y = np.max(self.cell_gradient_y, axis=1).data ** (-1.0) + self._cell_distances_y = self.cell_gradient_y.max(axis=1).data ** (-1.0) return self._cell_distances_y @@ -579,7 +579,7 @@ def cell_distances_z(self) -> np.ndarray: Cell center distance array along the z-direction. """ if getattr(self, "_cell_distances_z", None) is None: - self._cell_distances_z = np.max(self.cell_gradient_z, axis=1).data ** (-1.0) + self._cell_distances_z = self.cell_gradient_z.max(axis=1).data ** (-1.0) return self._cell_distances_z From a73f59312d936818c47511933e16398b7d065999 Mon Sep 17 00:00:00 2001 From: fourndo Date: Wed, 8 Nov 2023 10:16:48 -0800 Subject: [PATCH 311/455] Add unitest --- tests/base/test_regularization.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index ceb910baf2..4efb48174a 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -630,6 +630,16 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) +def test_coterminal_angle(): + mesh = discretize.TreeMesh([16, 16, 16]) + mesh.insert_cells([100, 100, 100], mesh.max_level, finalize=True) + + reg = regularization.SmoothnessFirstOrder(mesh, units="radian", orientation="y") + angles = np.ones(mesh.n_cells) * np.pi + angles[5] = -np.pi + assert np.all(reg.f_m(angles) == 0) + + class TestParent: """Test parent property of regularizations.""" From 211f627874c46d3a03450b840970b9a10a7fbe4b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 23 Nov 2023 11:34:45 -0800 Subject: [PATCH 312/455] Fix deprecation warning for gradientType in SparseSmoothness (#1284) Fix the message for the deprecation of `gradienType` for `gradient_type` in `SparseSmoothness` regularization. --- SimPEG/regularization/sparse.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 0ab1bb9a0b..e1602971c0 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -691,7 +691,12 @@ def gradient_type(self, value: str): ) gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=False, future_warn=True + gradient_type, + "gradientType", + new_name="gradient_type", + removal_version="0.19.0", + error=False, + future_warn=True, ) From eb07ef133a6c5e1714ff1af181147cb172eba97c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 7 Dec 2023 13:33:54 -0800 Subject: [PATCH 313/455] Gravity simulation with Choclo as engine (#1285) Add the option to use [Choclo](https://www.fatiando.org/choclo) as the engine for running the gravity simulations, considerably reducing the computation time of the forward simulations and the creation of the sensitivity matrix. Add Choclo as one of the optional dependencies. Update and extend tests for the gravity simulation using pytest. By default, the simulation will use `geoana` as the engine. Users can alternatively choose `choclo` instead if it's installed. --- .../gravity/_numba_functions.py | 250 ++++++++ SimPEG/potential_fields/gravity/simulation.py | 304 ++++++++- SimPEG/simulation.py | 2 +- SimPEG/utils/code_utils.py | 1 + environment_test.yml | 4 +- setup.py | 2 +- tests/pf/test_forward_Grav_Linear.py | 607 +++++++++++++----- tests/pf/test_grav_inversion_linear.py | 225 +++---- tests/utils/test_report.py | 1 + 9 files changed, 1095 insertions(+), 301 deletions(-) create mode 100644 SimPEG/potential_fields/gravity/_numba_functions.py diff --git a/SimPEG/potential_fields/gravity/_numba_functions.py b/SimPEG/potential_fields/gravity/_numba_functions.py new file mode 100644 index 0000000000..c84069f150 --- /dev/null +++ b/SimPEG/potential_fields/gravity/_numba_functions.py @@ -0,0 +1,250 @@ +""" +Numba functions for gravity simulation using Choclo. +""" +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + + +def _forward_gravity( + receivers, + nodes, + densities, + fields, + cell_nodes, + kernel_func, + constant_factor, +): + """ + Forward model the gravity field of active cells on receivers + + This function should be used with a `numba.jit` decorator, for example: + + ..code:: + + from numba import jit + + jit_forward_gravity = jit(nopython=True, parallel=True)(_forward_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + densities : (n_active_cells) numpy.ndarray + Array with densities of each active cell in the mesh. + fields : (n_receivers) numpy.ndarray + Array full of zeros where the gravity fields on each receiver will be + stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + ``fields`` array. + + Notes + ----- + The constant factor is applied here to each element of fields because + it's more efficient than doing it afterwards: it would require to + index the elements that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute fields from the kernel values + for k in range(n_cells): + fields[i] += ( + constant_factor + * densities[k] + * _kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, 0], + cell_nodes[k, 1], + cell_nodes[k, 2], + cell_nodes[k, 3], + cell_nodes[k, 4], + cell_nodes[k, 5], + cell_nodes[k, 6], + cell_nodes[k, 7], + ) + ) + + +def _sensitivity_gravity( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + kernel_func, + constant_factor, +): + """ + Fill the sensitivity matrix + + This function should be used with a `numba.jit` decorator, for example: + + ..code:: + + from numba import jit + + jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + + Notes + ----- + The constant factor is applied here to each row of the sensitivity matrix + because it's more efficient than doing it afterwards: it would require to + index the rows that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + sensitivity_matrix[i, k] = constant_factor * _kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, 0], + cell_nodes[k, 1], + cell_nodes[k, 2], + cell_nodes[k, 3], + cell_nodes[k, 4], + cell_nodes[k, 5], + cell_nodes[k, 6], + cell_nodes[k, 7], + ) + + +@jit(nopython=True) +def _evaluate_kernel( + receiver_x, receiver_y, receiver_z, node_x, node_y, node_z, kernel_func +): + """ + Evaluate a kernel function for a single node and receiver + + Parameters + ---------- + receiver_x, receiver_y, receiver_z : floats + Coordinates of the receiver. + node_x, node_y, node_z : floats + Coordinates of the node. + kernel_func : callable + Kernel function that should be evaluated. For example, use one of the + kernel functions in ``choclo.prism``. + + Returns + ------- + float + Kernel evaluated on the given node and receiver. + """ + dx = node_x - receiver_x + dy = node_y - receiver_y + dz = node_z - receiver_z + distance = np.sqrt(dx**2 + dy**2 + dz**2) + return kernel_func(dx, dy, dz, distance) + + +@jit(nopython=True) +def _kernels_in_nodes_to_cell( + kernels, + nodes_indices_0, + nodes_indices_1, + nodes_indices_2, + nodes_indices_3, + nodes_indices_4, + nodes_indices_5, + nodes_indices_6, + nodes_indices_7, +): + """ + Evaluate integral on a given cell from evaluation of kernels on nodes + + Parameters + ---------- + kernels : (n_active_nodes,) numpy.ndarray + Array with kernel values on each one of the nodes in the mesh. + nodes_indices : ints + Indices of the nodes for the current cell in "F" order (x changes + faster than y, and y faster than z). + + Returns + ------- + float + """ + result = ( + -kernels[nodes_indices_0] + + kernels[nodes_indices_1] + + kernels[nodes_indices_2] + - kernels[nodes_indices_3] + + kernels[nodes_indices_4] + - kernels[nodes_indices_5] + - kernels[nodes_indices_6] + + kernels[nodes_indices_7] + ) + return result + + +# Define decorated versions of these functions +_sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) +_sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) +_forward_gravity_parallel = jit(nopython=True, parallel=True)(_forward_gravity) +_forward_gravity_serial = jit(nopython=True, parallel=False)(_forward_gravity) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index c5f134e402..a41ea48112 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -1,4 +1,7 @@ +import os +import warnings import numpy as np +import discretize import scipy.constants as constants from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG @@ -9,6 +12,52 @@ from ...base import BasePDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation +from ._numba_functions import ( + choclo, + _sensitivity_gravity_serial, + _sensitivity_gravity_parallel, + _forward_gravity_serial, + _forward_gravity_parallel, +) + +if choclo is not None: + from numba import jit + + @jit(nopython=True) + def kernel_uv(easting, northing, upward, radius): + """Kernel for Guv gradiometry component.""" + result = 0.5 * ( + choclo.prism.kernel_nn(easting, northing, upward, radius) + - choclo.prism.kernel_ee(easting, northing, upward, radius) + ) + return result + + CHOCLO_KERNELS = { + "gx": choclo.prism.kernel_e, + "gy": choclo.prism.kernel_n, + "gz": choclo.prism.kernel_u, + "gxx": choclo.prism.kernel_ee, + "gyy": choclo.prism.kernel_nn, + "gzz": choclo.prism.kernel_uu, + "gxy": choclo.prism.kernel_en, + "gxz": choclo.prism.kernel_eu, + "gyz": choclo.prism.kernel_nu, + "guv": kernel_uv, + } + + +def _get_conversion_factor(component): + """ + Return conversion factor for the given component + """ + if component in ("gx", "gy", "gz"): + conversion_factor = 1e8 + elif component in ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"): + conversion_factor = 1e12 + else: + raise ValueError(f"Invalid component '{component}'.") + return conversion_factor + class Simulation3DIntegral(BasePFSimulation): """ @@ -27,27 +76,134 @@ class Simulation3DIntegral(BasePFSimulation): Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are returned in Eotvos (:math:`10^{-9} s^{-2}`). + + Parameters + ---------- + mesh : discretize.TreeMesh or discretize.TensorMesh + Mesh use to run the gravity simulation. + survey : SimPEG.potential_fields.gravity.Survey + Gravity survey with information of the receivers. + ind_active : (n_cells) numpy.ndarray, optional + Array that indicates which cells in ``mesh`` are active cells. + rho : numpy.ndarray (optional) + Density array for the active cells in the mesh. + rhoMap : Mapping (optional) + Model mapping. + sensitivity_dtype : numpy.dtype, optional + Data type that will be used to build the sensitivity matrix. + store_sensitivities : str + Options for storing sensitivity matrix. There are 3 options + + - 'ram': sensitivities are stored in the computer's RAM + - 'disk': sensitivities are written to a directory + - 'forward_only': you intend only do perform a forward simulation and + sensitivities do not need to be stored + + sensitivity_path : str, optional + Path to store the sensitivity matrix if ``store_sensitivities`` is set + to ``"disk"``. Default to "./sensitivities". + engine : str, optional + Choose which engine should be used to run the forward model: + ``"geoana"`` or "``choclo``". + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ rho, rhoMap, rhoDeriv = props.Invertible("Density") - def __init__(self, mesh, rho=None, rhoMap=None, **kwargs): + def __init__( + self, + mesh, + rho=None, + rhoMap=None, + engine="geoana", + numba_parallel=True, + **kwargs, + ): super().__init__(mesh, **kwargs) self.rho = rho self.rhoMap = rhoMap self._G = None self._gtg_diagonal = None self.modelMap = self.rhoMap + self.numba_parallel = numba_parallel + self.engine = engine + self._sanity_checks_engine(kwargs) + # Define jit functions + if self.engine == "choclo": + if numba_parallel: + self._sensitivity_gravity = _sensitivity_gravity_parallel + self._forward_gravity = _forward_gravity_parallel + else: + self._sensitivity_gravity = _sensitivity_gravity_serial + self._forward_gravity = _forward_gravity_serial + + def _sanity_checks_engine(self, kwargs): + """ + Sanity checks for the engine parameter. + + Needs the kwargs passed to the __init__ method to raise some warnings. + Will set n_processes to None if it's present in kwargs. + """ + if self.engine not in ("choclo", "geoana"): + raise ValueError( + f"Invalid engine '{self.engine}'. Choose from 'geoana' or 'choclo'." + ) + if self.engine == "choclo" and choclo is None: + raise ImportError( + "The choclo package couldn't be found." + "Running a gravity simulation with 'engine=\"choclo\"' needs " + "choclo to be installed." + "\nTry installing choclo with:" + "\n pip install choclo" + "\nor:" + "\n conda install choclo" + ) + # Warn if n_processes has been passed + if self.engine == "choclo" and "n_processes" in kwargs: + warnings.warn( + "The 'n_processes' will be ignored when selecting 'choclo' as the " + "engine in the gravity simulation.", + UserWarning, + stacklevel=1, + ) + self.n_processes = None + # Sanity checks for sensitivity_path when using choclo and storing in disk + if self.engine == "choclo" and self.store_sensitivities == "disk": + if os.path.isdir(self.sensitivity_path): + raise ValueError( + f"The passed sensitivity_path '{self.sensitivity_path}' is " + "a directory. " + "When using 'choclo' as the engine, 'senstivity_path' " + "should be the path to a new or existing file." + ) def fields(self, m): - self.model = m + """ + Forward model the gravity field of the mesh on the receivers in the survey + + Parameters + ---------- + m : (n_active_cells,) numpy.ndarray + Array with values for the model. + Returns + ------- + (nD,) numpy.ndarray + Gravity fields generated by the given model on every receiver + location. + """ + self.model = m if self.store_sensitivities == "forward_only": # Compute the linear operation without forming the full dense G - fields = mkvc(self.linear_operator()) + if self.engine == "choclo": + fields = self._forward(self.rho) + else: + fields = mkvc(self.linear_operator()) else: fields = self.G @ (self.rho).astype(self.sensitivity_dtype, copy=False) - return np.asarray(fields) def getJtJdiag(self, m, W=None, f=None): @@ -95,8 +251,10 @@ def G(self): Gravity forward operator """ if getattr(self, "_G", None) is None: - self._G = self.linear_operator() - + if self.engine == "choclo": + self._G = self._sensitivity_matrix() + else: + self._G = self.linear_operator() return self._G @property @@ -205,6 +363,140 @@ def evaluate_integral(self, receiver_location, components): ] ) + def _forward(self, densities): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + densities : (n_active_cells) numpy.ndarray + Array containing the densities of the active cells in the mesh, in + g/cc. + + Returns + ------- + (nD,) numpy.ndarray + Always return a ``np.float64`` array. + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Compute fields + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_elements = n_components * receivers.shape[0] + for i, component in enumerate(components): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_elements, n_components + ) + self._forward_gravity( + receivers, + active_nodes, + densities, + fields[vector_slice], + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + ) + index_offset += n_elements + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate sensitivity matrix + shape = (self.survey.nD, self.nC) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Start filling the sensitivity matrix + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + self._sensitivity_gravity( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + ) + index_offset += n_rows + return sensitivity_matrix + + def _get_cell_nodes(self): + """ + Return indices of nodes for each cell in the mesh. + """ + if not isinstance(self.mesh, (discretize.TreeMesh, discretize.TensorMesh)): + raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") + cell_nodes = self.mesh.cell_nodes + return cell_nodes + + def _get_active_nodes(self): + """ + Return locations of nodes only for active cells + + Also return an array containing the indices of the "active nodes" for + each active cell in the mesh + """ + # Get all nodes in the mesh + if isinstance(self.mesh, discretize.TreeMesh): + nodes = self.mesh.total_nodes + elif isinstance(self.mesh, discretize.TensorMesh): + nodes = self.mesh.nodes + else: + raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") + # Get original cell_nodes but only for active cells + cell_nodes = self._get_cell_nodes() + # If all cells in the mesh are active, return nodes and cell_nodes + if self.nC == self.mesh.n_cells: + return nodes, cell_nodes + # Keep only the cell_nodes for active cells + cell_nodes = cell_nodes[self.ind_active] + # Get the unique indices of the nodes that belong to every active cell + # (these indices correspond to the original `nodes` array) + unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) + # Select only the nodes that belong to the active cells (active nodes) + active_nodes = nodes[unique_nodes] + # Reshape indices of active cells for each active cell in the mesh + active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) + return active_nodes, active_cell_nodes + + def _get_components_and_receivers(self): + """Generator for receiver locations and their field components.""" + if not hasattr(self.survey, "source_field"): + raise AttributeError( + f"The survey '{self.survey}' has no 'source_field' attribute." + ) + for receiver_object in self.survey.source_field.receiver_list: + yield receiver_object.components, receiver_object.locations + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index 7e88483b98..16c9e3d7d1 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -101,7 +101,7 @@ def counter(self, value): @property def sensitivity_path(self): - """Path to store the sensitivty. + """Path to store the sensitivity. Returns ------- diff --git a/SimPEG/utils/code_utils.py b/SimPEG/utils/code_utils.py index b0c5623a4c..87e4c1bede 100644 --- a/SimPEG/utils/code_utils.py +++ b/SimPEG/utils/code_utils.py @@ -491,6 +491,7 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "vtk", "utm", "memory_profiler", + "choclo", ] super().__init__( diff --git a/environment_test.yml b/environment_test.yml index b658b30427..888c2180a0 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -7,7 +7,7 @@ dependencies: - scikit-learn>=1.2 - pymatsolver>=0.2 - matplotlib - - discretize>=0.8 + - discretize>=0.10 - geoana>=0.5.0 - empymod>=2.0.0 - setuptools_scm @@ -36,6 +36,8 @@ dependencies: - pyvista - pip - python-kaleido + # Optional dependencies + - choclo # Linters and code style - pre-commit - black==23.1.0 diff --git a/setup.py b/setup.py index 1a2459028e..9e55ae825a 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ "scikit-learn>=1.2", "pymatsolver>=0.2", "matplotlib", - "discretize>=0.8", + "discretize>=0.10", "geoana>=0.5.0", "empymod>=2.0.0", "pandas", diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 286e8ba6db..496e80b1f7 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,29 +1,63 @@ -import unittest +from unittest.mock import patch import pytest import discretize from SimPEG import maps from SimPEG.potential_fields import gravity from geoana.gravity import Prism import numpy as np -import os -def test_ana_grav_forward(tmp_path): - nx = 5 - ny = 5 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): +class TestsGravitySimulation: + """ + Test gravity simulation. + """ + + @pytest.fixture + def blocks(self): + """Synthetic blocks to build the sample model.""" + block1 = np.array([[-1.6, 1.6], [-1.6, 1.6], [-1.6, 1.6]]) + block2 = np.array([[-0.8, 0.8], [-0.8, 0.8], [-0.8, 0.8]]) + rho1 = 1.0 + rho2 = 2.0 + return (block1, block2), (rho1, rho2) + + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, blocks, request): + """Sample mesh.""" + cs = 0.2 + (block1, _), _ = blocks + if request.param == "tensormesh": + hxind, hyind, hzind = tuple([(cs, 42)] for _ in range(3)) + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + else: + h = cs * np.ones(64) + mesh = discretize.TreeMesh([h, h, h], origin="CCC") + x0, x1 = block1[:, 0], block1[:, 1] + mesh.refine_box(x0, x1, levels=9) + return mesh + + @pytest.fixture + def simple_mesh(self): + """Simpler sample mesh, just to use it as a placeholder in some tests.""" + return discretize.TensorMesh([5, 5, 5], "CCC") + + @pytest.fixture + def density_and_active_cells(self, mesh, blocks): + """Sample density and active_cells arrays for the sample mesh.""" + # create a model of two blocks, 1 inside the other + (block1, block2), (rho1, rho2) = blocks + block1_inds = self.get_block_inds(mesh.cell_centers, block1) + block2_inds = self.get_block_inds(mesh.cell_centers, block2) + # Define densities for each block + model = np.zeros(mesh.n_cells) + model[block1_inds] = rho1 + model[block2_inds] = rho2 + # Define active cells and reduce model + active_cells = model != 0.0 + model_reduced = model[active_cells] + return model_reduced, active_cells + + def get_block_inds(self, grid, block): return np.where( (grid[:, 0] > block[0, 0]) & (grid[:, 0] < block[0, 1]) @@ -33,160 +67,395 @@ def get_block_inds(grid, block): & (grid[:, 2] < block[2, 1]) ) - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - rho1 = 1.0 - rho2 = 2.0 - - model = np.zeros(mesh.n_cells) - model[block1_inds] = rho1 - model[block2_inds] = rho2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Pproblem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - - receivers = gravity.Point(locXyz, components=["gx", "gy", "gz"]) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - - sim = gravity.Simulation3DIntegral( + @pytest.fixture + def receivers_locations(self): + nx = 5 + ny = 5 + # Create plane of observations + xr = np.linspace(-20, 20, nx) + yr = np.linspace(-20, 20, ny) + x, y = np.meshgrid(xr, yr) + z = np.ones_like(x) * 3.0 + receivers_locations = np.vstack([a.ravel() for a in (x, y, z)]).T + return receivers_locations + + def get_analytic_solution(self, blocks, survey): + """Compute analytical response from dense prism.""" + (block1, block2), (rho1, rho2) = blocks + # Build prisms (convert densities from g/cc to kg/m3) + prisms = [ + Prism(block1[:, 0], block1[:, 1], rho1 * 1000), + Prism(block2[:, 0], block2[:, 1], -rho1 * 1000), + Prism(block2[:, 0], block2[:, 1], rho2 * 1000), + ] + # Forward model the prisms + components = survey.source_field.receiver_list[0].components + receivers_locations = survey.source_field.receiver_list[0].locations + if "gx" in components or "gy" in components or "gz" in components: + fields = sum( + prism.gravitational_field(receivers_locations) for prism in prisms + ) + fields *= 1e5 # convert to mGal from m/s^2 + else: + fields = sum( + prism.gravitational_gradient(receivers_locations) for prism in prisms + ) + fields *= 1e9 # convert to Eotvos from 1/s^2 + return fields + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", None), ("geoana", 1), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_accelerations_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - store_sensitivities="disk", - sensitivity_path=str(tmp_path) + os.sep, + density_and_active_cells, + receivers_locations, + ): + """ + Test gravity acceleration components against analytic solutions of prisms. + """ + components = ["gx", "gy", "gz"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, + ) + data = sim.dpred(density) + g_x, g_y, g_z = data[0::3], data[1::3], data[2::3] + solution = self.get_analytic_solution(blocks, survey) + # Check results + rtol, atol = 1e-9, 1e-6 + np.testing.assert_allclose(g_x, solution[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_y, solution[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_z, solution[:, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", None), ("geoana", 1), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - - with pytest.raises(TypeError): - sim.sensitivity_dtype = float - - assert sim.sensitivity_dtype is np.float32 - - data = sim.dpred(model_reduced) - d_x = data[0::3] - d_y = data[1::3] - d_z = data[2::3] - - # Compute analytical response from dense prism - prism_1 = Prism(block1[:, 0], block1[:, 1], rho1 * 1000) # g/cc to kg/m**3 - prism_2 = Prism(block2[:, 0], block2[:, 1], -rho1 * 1000) - prism_3 = Prism(block2[:, 0], block2[:, 1], rho2 * 1000) - - d = ( - prism_1.gravitational_field(locXyz) - + prism_2.gravitational_field(locXyz) - + prism_3.gravitational_field(locXyz) - ) * 1e5 # convert to mGal from m/s^2 - d = d.astype(sim.sensitivity_dtype) - np.testing.assert_allclose(d_x, d[:, 0], rtol=1e-9, atol=1e-6) - np.testing.assert_allclose(d_y, d[:, 1], rtol=1e-9, atol=1e-6) - np.testing.assert_allclose(d_z, d[:, 2], rtol=1e-9, atol=1e-6) - - -def test_ana_gg_forward(): - nx = 5 - ny = 5 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tensor_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, + mesh, + density_and_active_cells, + receivers_locations, + ): + """ + Test tensor components against analytic solutions of prisms. + """ + components = ["gxx", "gxy", "gxz", "gyy", "gyz", "gzz"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - rho1 = 1.0 - rho2 = 2.0 - - model = np.zeros(mesh.n_cells) - model[block1_inds] = rho1 - model[block2_inds] = rho2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Pproblem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - - receivers = gravity.Point( - locXyz, components=["gxx", "gxy", "gxz", "gyy", "gyz", "gzz"] + data = sim.dpred(density) + g_xx, g_xy, g_xz = data[0::6], data[1::6], data[2::6] + g_yy, g_yz, g_zz = data[3::6], data[4::6], data[5::6] + solution = self.get_analytic_solution(blocks, survey) + # Check results + rtol, atol = 2e-6, 1e-6 + np.testing.assert_allclose(g_xx, solution[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_xy, solution[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_xz, solution[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_yy, solution[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_yz, solution[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_zz, solution[..., 2, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", 1), ("geoana", None), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - - sim = gravity.Simulation3DIntegral( + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_guv_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + density_and_active_cells, + receivers_locations, + ): + """ + Test guv tensor component against analytic solutions of prisms. + """ + components = ["guv"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, + ) + g_uv = sim.dpred(density) + solution = self.get_analytic_solution(blocks, survey) + g_xx_solution = solution[..., 0, 0] + g_yy_solution = solution[..., 1, 1] + g_uv_solution = 0.5 * (g_yy_solution - g_xx_solution) + # Check results + rtol, atol = 2e-6, 1e-6 + np.testing.assert_allclose(g_uv, g_uv_solution, rtol=rtol, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_sensitivity_dtype( + self, + engine, + store_sensitivities, + simple_mesh, + receivers_locations, + tmp_path, + ): + """Test sensitivity_dtype.""" + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Create simulation + sensitivity_path = tmp_path + if engine == "choclo": + sensitivity_path /= "dummy" + simulation = gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + engine=engine, + store_sensitivities=store_sensitivities, + sensitivity_path=str(sensitivity_path), + ) + # sensitivity_dtype should be float64 when running forward only, + # but float32 in other cases + if store_sensitivities == "forward_only": + assert simulation.sensitivity_dtype is np.float64 + else: + assert simulation.sensitivity_dtype is np.float32 + + @pytest.mark.parametrize("invalid_dtype", (float, np.float16)) + def test_invalid_sensitivity_dtype_assignment( + self, simple_mesh, receivers_locations, invalid_dtype + ): + """ + Test invalid sensitivity_dtype assignment + """ + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Create simulation + simulation = gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + ) + # Check if error is raised + msg = "sensitivity_dtype must be either np.float32 or np.float64." + with pytest.raises(TypeError, match=msg): + simulation.sensitivity_dtype = invalid_dtype + + def test_invalid_engine(self, simple_mesh, receivers_locations): + """Test if error is raised after invalid engine.""" + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Check if error is raised after an invalid engine is passed + engine = "invalid engine" + with pytest.raises(ValueError, match=f"Invalid engine '{engine}'"): + gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + engine=engine, + ) + + def test_choclo_and_n_proceesses(self, simple_mesh, receivers_locations): + """Check if warning is raised after passing n_processes with choclo engine.""" + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Check if warning is raised + msg = "The 'n_processes' will be ignored when selecting 'choclo'" + with pytest.warns(UserWarning, match=msg): + simulation = gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + engine="choclo", + n_processes=2, + ) + # Check if n_processes was overwritten and set to None + assert simulation.n_processes is None + + def test_choclo_and_sensitivity_path_as_dir( + self, simple_mesh, receivers_locations, tmp_path + ): + """ + Check if error is raised when sensitivity_path is a dir with choclo engine. + """ + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Create a sensitivity_path directory + sensitivity_path = tmp_path / "sensitivity_dummy" + sensitivity_path.mkdir() + # Check if error is raised + msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + with pytest.raises(ValueError, match=msg): + gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + engine="choclo", + ) + + @patch("SimPEG.potential_fields.gravity.simulation.choclo", None) + def test_choclo_missing(self, simple_mesh, receivers_locations): + """ + Check if error is raised when choclo is missing and chosen as engine. + """ + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Check if error is raised + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + engine="choclo", + ) + + +class TestConversionFactor: + """Test _get_conversion_factor function.""" + + @pytest.mark.parametrize( + "component", + ("gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"), ) - - # forward only should default to np.float64 - assert sim.sensitivity_dtype is np.float64 - - data = sim.dpred(model_reduced) - d_xx = data[0::6] - d_xy = data[1::6] - d_xz = data[2::6] - d_yy = data[3::6] - d_yz = data[4::6] - d_zz = data[5::6] - - # Compute analytical response from dense prism - prism_1 = Prism(block1[:, 0], block1[:, 1], rho1 * 1000) # g/cc to kg/m**3 - prism_2 = Prism(block2[:, 0], block2[:, 1], -rho1 * 1000) - prism_3 = Prism(block2[:, 0], block2[:, 1], rho2 * 1000) - - d = ( - prism_1.gravitational_gradient(locXyz) - + prism_2.gravitational_gradient(locXyz) - + prism_3.gravitational_gradient(locXyz) - ) * 1e9 # convert to Eotvos from 1/s^2 - - np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=1e-10, atol=1e-12) - - -if __name__ == "__main__": - unittest.main() + def test_conversion_factor(self, component): + """ + Test _get_conversion_factor function with valid components + """ + conversion_factor = gravity.simulation._get_conversion_factor(component) + if len(component) == 2: + assert conversion_factor == 1e5 * 1e3 # SI to mGal and g/cc to kg/m3 + else: + assert conversion_factor == 1e9 * 1e3 # SI to Eotvos and g/cc to kg/m3 + + def test_invalid_conversion_factor(self): + """ + Test invalid conversion factor _get_conversion_factor function + """ + component = "invalid-component" + with pytest.raises(ValueError, match=f"Invalid component '{component}'"): + gravity.simulation._get_conversion_factor(component) diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 4c98e6aecc..c67a8ba474 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -1,5 +1,4 @@ -import shutil -import unittest +import pytest import numpy as np import discretize @@ -17,125 +16,105 @@ from SimPEG.potential_fields import gravity -class GravInvLinProblemTest(unittest.TestCase): - def setUp(self): - # Create a self.mesh - dx = 5.0 - hxind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] - hyind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] - hzind = [(dx, 5, -1.3), (dx, 6)] - self.mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # Get index of the center - midx = int(self.mesh.shape_cells[0] / 2) - midy = int(self.mesh.shape_cells[1] / 2) - - # Lets create a simple Gaussian topo and set the active cells - [xx, yy] = np.meshgrid(self.mesh.nodes_x, self.mesh.nodes_y) - zz = -np.exp((xx**2 + yy**2) / 75**2) + self.mesh.nodes_z[-1] - - # Go from topo to actv cells - topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] - actv = active_from_xyz(self.mesh, topo, "N") - - # Create active map to go from reduce space to full - self.actvMap = maps.InjectActiveCells(self.mesh, actv, -100) - nC = int(actv.sum()) - - # Create and array of observation points - xr = np.linspace(-20.0, 20.0, 20) - yr = np.linspace(-20.0, 20.0, 20) - X, Y = np.meshgrid(xr, yr) - - # Move the observation points 5m above the topo - Z = -np.exp((X**2 + Y**2) / 75**2) + self.mesh.nodes_z[-1] + 5.0 - - # Create a MAGsurvey - locXYZ = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] - rxLoc = gravity.Point(locXYZ) - srcField = gravity.SourceField([rxLoc]) - survey = gravity.Survey(srcField) - - # We can now create a density model and generate data - # Here a simple block in half-space - model = np.zeros( - ( - self.mesh.shape_cells[0], - self.mesh.shape_cells[1], - self.mesh.shape_cells[2], - ) +@pytest.mark.parametrize("engine", ("geoana", "choclo")) +def test_gravity_inversion_linear(engine): + """Test gravity inversion.""" + # Create a mesh + dx = 5.0 + hxind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] + hyind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] + hzind = [(dx, 5, -1.3), (dx, 6)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + + # Get index of the center + midx = int(mesh.shape_cells[0] / 2) + midy = int(mesh.shape_cells[1] / 2) + + # Lets create a simple Gaussian topo and set the active cells + [xx, yy] = np.meshgrid(mesh.nodes_x, mesh.nodes_y) + zz = -np.exp((xx**2 + yy**2) / 75**2) + mesh.nodes_z[-1] + + # Go from topo to actv cells + topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + actv = active_from_xyz(mesh, topo, "N") + nC = int(actv.sum()) + + # Create and array of observation points + xr = np.linspace(-20.0, 20.0, 20) + yr = np.linspace(-20.0, 20.0, 20) + X, Y = np.meshgrid(xr, yr) + + # Move the observation points 5m above the topo + Z = -np.exp((X**2 + Y**2) / 75**2) + mesh.nodes_z[-1] + 5.0 + + # Create a MAGsurvey + locXYZ = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] + rxLoc = gravity.Point(locXYZ) + srcField = gravity.SourceField([rxLoc]) + survey = gravity.Survey(srcField) + + # We can now create a density model and generate data + # Here a simple block in half-space + model = np.zeros( + ( + mesh.shape_cells[0], + mesh.shape_cells[1], + mesh.shape_cells[2], ) - model[(midx - 2) : (midx + 2), (midy - 2) : (midy + 2), -6:-2] = 0.5 - model = utils.mkvc(model) - self.model = model[actv] - - # Create reduced identity map - idenMap = maps.IdentityMap(nP=nC) - - # Create the forward model operator - sim = gravity.Simulation3DIntegral( - self.mesh, - survey=survey, - rhoMap=idenMap, - ind_active=actv, - store_sensitivities="ram", - n_processes=None, - ) - - # Compute linear forward operator and compute some data - data = sim.make_synthetic_data( - self.model, - relative_error=0.0, - noise_floor=0.0005, - add_noise=True, - random_seed=2, - ) - - # Create a regularization - reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) - reg.norms = [0, 0, 0, 0] - reg.gradientType = "components" - - # Data misfit function - dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) - - # Add directives to the inversion - opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 - ) - invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) - - # Here is where the norms are applied - starting_beta = directives.BetaEstimateMaxDerivative(10.0) - IRLS = directives.Update_IRLS() - update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) - self.inv = inversion.BaseInversion( - invProb, - directiveList=[IRLS, sensitivity_weights, starting_beta, update_Jacobi], - ) - self.sim = sim - - def test_grav_inverse(self): - # Run the inversion - mrec = self.inv.run(self.model) - residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) - - # import matplotlib.pyplot as plt - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, clim=(0, 0.5), normal="Y") - # ax = plt.subplot(1, 2, 2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, clim=(0, 0.5), normal="Y") - # plt.show() - - self.assertTrue(residual < 0.05) - - def tearDown(self): - # Clean up the working directory - if self.sim.store_sensitivities == "disk": - shutil.rmtree(self.sim.sensitivity_path) - - -if __name__ == "__main__": - unittest.main() + ) + model[(midx - 2) : (midx + 2), (midy - 2) : (midy + 2), -6:-2] = 0.5 + model = utils.mkvc(model) + model = model[actv] + + # Create reduced identity map + idenMap = maps.IdentityMap(nP=nC) + + # Create the forward model operator + kwargs = dict() + if engine == "geoana": + kwargs["n_processes"] = None + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=actv, + store_sensitivities="ram", + engine=engine, + **kwargs, + ) + + # Compute linear forward operator and compute some data + data = sim.make_synthetic_data( + model, relative_error=0.0, noise_floor=0.0005, add_noise=True, random_seed=2 + ) + + # Create a regularization + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) + reg.norms = [0, 0, 0, 0] + reg.gradient_type = "components" + + # Data misfit function + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + # Add directives to the inversion + opt = optimization.ProjectedGNCG( + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + ) + invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) + + # Here is where the norms are applied + starting_beta = directives.BetaEstimateMaxDerivative(10.0) + IRLS = directives.Update_IRLS() + update_Jacobi = directives.UpdatePreconditioner() + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) + inv = inversion.BaseInversion( + invProb, + directiveList=[IRLS, sensitivity_weights, starting_beta, update_Jacobi], + ) + + # Run the inversion + mrec = inv.run(model) + residual = np.linalg.norm(mrec - model) / np.linalg.norm(model) + + # Assert result + assert np.all(residual < 0.05) diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index e9ebe09bb3..9d5091545e 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -34,6 +34,7 @@ def test_version_defaults(self): "vtk", "utm", "memory_profiler", + "choclo", ], ncol=3, text_width=80, From 102f32356260d15bbd90f748948155fbfd6676bd Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 8 Dec 2023 09:25:44 -0800 Subject: [PATCH 314/455] Fix minor flake8 warning (#1307) Fix a minor flake8 warning in one of the examples that was making the style checks in GitHub Actions to fail. --- examples/20-published/plot_heagyetal2017_casing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index 58d76f70ab..6d0543130c 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -618,7 +618,7 @@ def solveSecondary(self, sec_problem, sec_survey, m, plotIt=False): fields = sec_problem.fields(m) dpred = sec_problem.dpred(m, f=fields) t1 = time.time() - print(" ...done. secondary time "), t1 - t0 + print(f" ...done. secondary time {t1 - t0}") return fields, dpred From 1616d2c6874d5cc12e90cf55581f4b25d2a21337 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 27 Dec 2023 11:19:13 -0800 Subject: [PATCH 315/455] switch tutorials rand to randn --- tutorials/03-gravity/plot_1a_gravity_anomaly.py | 2 +- tutorials/04-magnetics/plot_2a_magnetics_induced.py | 2 +- tutorials/05-dcr/plot_fwd_1_dcr_sounding.py | 2 +- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 2 +- tutorials/05-dcr/plot_fwd_3_dcr3d.py | 2 +- tutorials/06-ip/plot_fwd_2_dcip2d.py | 4 ++-- tutorials/06-ip/plot_fwd_3_dcip3d.py | 4 ++-- tutorials/07-fdem/plot_fwd_1_em1dfm.py | 2 +- tutorials/07-fdem/plot_fwd_3_fem_3d.py | 4 ++-- tutorials/08-tdem/plot_fwd_1_em1dtm.py | 2 +- tutorials/08-tdem/plot_fwd_3_tem_3d.py | 2 +- tutorials/12-seismic/plot_fwd_1_tomography_2D.py | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index a017eed964..41aa7cc1bd 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -232,6 +232,6 @@ np.random.seed(737) maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.01 * maximum_anomaly * np.random.rand(len(dpred)) + noise = 0.01 * maximum_anomaly * np.random.randn(len(dpred)) fname = dir_path + "gravity_data.obs" np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index eaab3b8a9b..42e6cd1b68 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -228,6 +228,6 @@ np.random.seed(211) maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.02 * maximum_anomaly * np.random.rand(len(dpred)) + noise = 0.02 * maximum_anomaly * np.random.randn(len(dpred)) fname = dir_path + "magnetics_data.obs" np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") diff --git a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py index 62590a5fa5..b97cbad3b9 100644 --- a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py @@ -157,7 +157,7 @@ os.mkdir(dir_path) np.random.seed(145) - noise = 0.025 * dpred * np.random.rand(len(dpred)) + noise = 0.025 * dpred * np.random.randn(len(dpred)) data_array = np.c_[ survey.locations_a, diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 3d58f7872a..259b5b6137 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -307,7 +307,7 @@ # Add 10% Gaussian noise to each datum np.random.seed(225) std = 0.05 * np.abs(dpred) - dc_noise = std * np.random.rand(len(dpred)) + dc_noise = std * np.random.randn(len(dpred)) dobs = dpred + dc_noise # Create a survey with the original electrode locations diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index f02757a50f..ea9064813c 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -347,7 +347,7 @@ # Add 5% Gaussian noise to each datum np.random.seed(433) std = 0.1 * np.abs(dpred) - noise = std * np.random.rand(len(dpred)) + noise = std * np.random.randn(len(dpred)) dobs = dpred + noise # Create dictionary that stores line IDs diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index ad19acb46b..eb7bac5aa6 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -473,7 +473,7 @@ # Add 5% Gaussian noise to each DC datum np.random.seed(225) std = 0.05 * np.abs(dpred_dc) - dc_noise = std * np.random.rand(len(dpred_dc)) + dc_noise = std * np.random.randn(len(dpred_dc)) dobs = dpred_dc + dc_noise # Create a survey with the original electrode locations @@ -497,7 +497,7 @@ # Add Gaussian noise equal to 5e-3 V/V std = 5e-3 * np.ones_like(dpred_ip) - ip_noise = std * np.random.rand(len(dpred_ip)) + ip_noise = std * np.random.randn(len(dpred_ip)) dobs = dpred_ip + ip_noise # Create a survey with the original electrode locations diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index f147ff64ed..d7d9aebd78 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -505,7 +505,7 @@ # Add 10% Gaussian noise to each datum np.random.seed(433) std = 0.1 * np.abs(dpred_dc) - noise = std * np.random.rand(len(dpred_dc)) + noise = std * np.random.randn(len(dpred_dc)) dobs = dpred_dc + noise # Create dictionary that stores line IDs @@ -543,7 +543,7 @@ # Add Gaussian noise with a standard deviation of 5e-3 V/V np.random.seed(444) std = 5e-3 * np.ones_like(dpred_ip) - noise = std * np.random.rand(len(dpred_ip)) + noise = std * np.random.randn(len(dpred_ip)) dobs = dpred_ip + noise # Create a survey with the original electrode locations diff --git a/tutorials/07-fdem/plot_fwd_1_em1dfm.py b/tutorials/07-fdem/plot_fwd_1_em1dfm.py index 6abd237d0c..bcb0254477 100644 --- a/tutorials/07-fdem/plot_fwd_1_em1dfm.py +++ b/tutorials/07-fdem/plot_fwd_1_em1dfm.py @@ -182,7 +182,7 @@ os.mkdir(dir_path) np.random.seed(222) - noise = 0.05 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.05 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dfm_data.txt" diff --git a/tutorials/07-fdem/plot_fwd_3_fem_3d.py b/tutorials/07-fdem/plot_fwd_3_fem_3d.py index d2bd523730..6dd4e7513f 100644 --- a/tutorials/07-fdem/plot_fwd_3_fem_3d.py +++ b/tutorials/07-fdem/plot_fwd_3_fem_3d.py @@ -312,8 +312,8 @@ # Write data with 2% noise added fname = dir_path + "fdem_data.obs" - bz_real = bz_real + 1e-14 * np.random.rand(len(bz_real)) - bz_imag = bz_imag + 1e-14 * np.random.rand(len(bz_imag)) + bz_real = bz_real + 1e-14 * np.random.randn(len(bz_real)) + bz_imag = bz_imag + 1e-14 * np.random.randn(len(bz_imag)) f_vec = np.kron(frequencies, np.ones(ntx)) receiver_locations = np.kron(np.ones((len(frequencies), 1)), receiver_locations) diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm.py b/tutorials/08-tdem/plot_fwd_1_em1dtm.py index d217835dad..7700d32745 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm.py @@ -168,7 +168,7 @@ os.mkdir(dir_path) np.random.seed(347) - noise = 0.05 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.05 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dtm_data.txt" np.savetxt(fname, np.c_[times, dpred], fmt="%.4e", header="TIME BZ") diff --git a/tutorials/08-tdem/plot_fwd_3_tem_3d.py b/tutorials/08-tdem/plot_fwd_3_tem_3d.py index b711465f86..15b7609a0e 100644 --- a/tutorials/08-tdem/plot_fwd_3_tem_3d.py +++ b/tutorials/08-tdem/plot_fwd_3_tem_3d.py @@ -364,7 +364,7 @@ # Write data with 2% noise added fname = dir_path + "tdem_data.obs" - dpred = dpred + 0.02 * np.abs(dpred) * np.random.rand(len(dpred)) + dpred = dpred + 0.02 * np.abs(dpred) * np.random.randn(len(dpred)) t_vec = np.kron(np.ones(ntx), time_channels) receiver_locations = np.kron(receiver_locations, np.ones((len(time_channels), 1))) diff --git a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py index c5a1fdff7a..083b653af2 100644 --- a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py @@ -170,7 +170,7 @@ dir_path.extend(["tutorials", "seismic", "assets"]) dir_path = os.path.sep.join(dir_path) + os.path.sep - noise = 0.05 * dpred * np.random.rand(len(dpred)) + noise = 0.05 * dpred * np.random.randn(len(dpred)) data_array = np.c_[ np.kron(x, np.ones(n_receiver)), From ba0c639d29ee6145b7953f3197159c80010fe382 Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 29 Dec 2023 09:35:55 -0800 Subject: [PATCH 316/455] One more randn --- tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py index 24400ea467..06869ae165 100644 --- a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py @@ -304,7 +304,7 @@ def PolygonInd(mesh, pts): dir_path.extend(["tutorials", "08-tdem", "em1dtm_stitched_skytem"]) dir_path = os.path.sep.join(dir_path) + os.path.sep - noise = 0.1 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.1 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dtm_stitched_skytem_data.obs" From 182ef1e63e5f7637992edc92154e84612164695b Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 3 Jan 2024 09:06:17 -0800 Subject: [PATCH 317/455] Ditch deprecated functions in utils.model_builder (#1311) Addresses name change for functions in `utils.model_builder`. Update tutorials, examples and tests that were using the deprecated functions. Related to issue #1265 --------- Co-authored-by: Santiago Soler --- .../natural_source/utils/test_utils.py | 2 +- .../static/utils/static_utils.py | 4 +- SimPEG/utils/model_builder.py | 60 +------------------ examples/01-maps/plot_mesh2mesh.py | 2 +- examples/01-maps/plot_sumMap.py | 2 +- examples/02-gravity/plot_inv_grav_tiled.py | 2 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 2 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 2 +- .../plot_inv_mag_nonLinear_Amplitude.py | 2 +- examples/07-nsem/plot_fwd_nsem_MTTipper3D.py | 2 +- .../plot_tomo_joint_with_volume.py | 2 +- .../_archived/plot_inv_dcip_2_5Dinversion.py | 6 +- ...lot_inv_dcip_dipoledipole_2_5Dinversion.py | 4 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 4 +- tests/dask/test_mag_MVI_Octree.py | 2 +- .../dask/test_mag_inversion_linear_Octree.py | 2 +- tests/dask/test_mag_nonLinear_Amplitude.py | 2 +- tests/em/static/test_DC_2D_analytic.py | 4 +- .../static/test_DC_FieldsDipoleFullspace.py | 8 +-- .../test_DC_FieldsMultipoleFullspace.py | 8 +-- tests/em/static/test_IP_2D_fwd.py | 4 +- tests/em/static/test_IP_fwd.py | 4 +- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 12 ++-- tests/em/static/test_SIP_jvecjtvecadj.py | 12 ++-- tests/pf/test_forward_PFproblem.py | 4 +- tests/pf/test_mag_MVI_Octree.py | 2 +- tests/pf/test_mag_inversion_linear_Octree.py | 2 +- tests/pf/test_mag_nonLinear_Amplitude.py | 2 +- tests/pf/test_mag_vector_amplitude.py | 2 +- tests/pf/test_magnetics_analytics.py | 4 +- tests/pf/test_pf_quadtree_inversion_linear.py | 4 +- tests/pf/test_sensitivity_PFproblem.py | 4 +- tests/seis/test_tomo.py | 2 +- .../01-models_mapping/plot_1_tensor_models.py | 8 ++- .../01-models_mapping/plot_3_tree_models.py | 8 ++- .../03-gravity/plot_1a_gravity_anomaly.py | 4 +- .../03-gravity/plot_1b_gravity_gradiometry.py | 4 +- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 4 +- .../plot_inv_1b_gravity_anomaly_irls.py | 4 +- .../04-magnetics/plot_2a_magnetics_induced.py | 2 +- .../04-magnetics/plot_2b_magnetics_mvi.py | 2 +- .../plot_inv_2a_magnetics_induced.py | 2 +- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 6 +- tutorials/05-dcr/plot_fwd_3_dcr3d.py | 4 +- tutorials/05-dcr/plot_inv_2_dcr2d.py | 6 +- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 6 +- tutorials/05-dcr/plot_inv_3_dcr3d.py | 4 +- tutorials/06-ip/plot_fwd_2_dcip2d.py | 8 ++- tutorials/06-ip/plot_fwd_3_dcip3d.py | 6 +- tutorials/06-ip/plot_inv_2_dcip2d.py | 6 +- tutorials/06-ip/plot_inv_3_dcip3d.py | 6 +- .../12-seismic/plot_fwd_1_tomography_2D.py | 2 +- 52 files changed, 121 insertions(+), 151 deletions(-) diff --git a/SimPEG/electromagnetics/natural_source/utils/test_utils.py b/SimPEG/electromagnetics/natural_source/utils/test_utils.py index e446ff7ad4..577ae25c1b 100644 --- a/SimPEG/electromagnetics/natural_source/utils/test_utils.py +++ b/SimPEG/electromagnetics/natural_source/utils/test_utils.py @@ -547,7 +547,7 @@ def blockInhalfSpace(conds): ccM = M.gridCC # conds = [1e-2] groundInd = ccM[:, 2] < elev - sig = utils.model_builder.defineBlock( + sig = utils.model_builder.create_block_in_wholespace( M.gridCC, np.array([-1000, -1000, -1500]), np.array([1000, 1000, -1000]), conds ) sig[~groundInd] = 1e-8 diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/SimPEG/electromagnetics/static/utils/static_utils.py index 886ec6a167..c52aaf81d2 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/SimPEG/electromagnetics/static/utils/static_utils.py @@ -1676,13 +1676,13 @@ def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): mesh2D = discretize.TensorMesh( [mesh.h[0], mesh.h[1]], x0=[mesh.x0[0], mesh.x0[1]] ) - out = model_builder.randomModel( + out = model_builder.create_random_model( mesh.vnC[:2], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy ) return out, mesh2D elif mesh.dim == 2: mesh1D = discretize.TensorMesh([mesh.h[0]], x0=[mesh.x0[0]]) - out = model_builder.randomModel( + out = model_builder.create_random_model( mesh.vnC[:1], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy ) return out, mesh1D diff --git a/SimPEG/utils/model_builder.py b/SimPEG/utils/model_builder.py index 60d2e26afc..e9670319bf 100644 --- a/SimPEG/utils/model_builder.py +++ b/SimPEG/utils/model_builder.py @@ -3,7 +3,6 @@ import scipy.sparse as sp from .mat_utils import mkvc from scipy.spatial import Delaunay -from .code_utils import deprecate_function from discretize.base import BaseMesh @@ -144,7 +143,7 @@ def create_block_in_wholespace( pass sigma = np.zeros(cell_centers.shape[0]) + background_value - ind = getIndicesBlock(p0, p1, cell_centers) + ind = get_indices_block(p0, p1, cell_centers) sigma[ind] = block_value @@ -317,7 +316,7 @@ def create_2_layer_model(cell_centers, depth, top_value=1.0, bottom_value=0.0): # The depth is always defined on the last one. p1[len(p1) - 1] -= depth - ind = getIndicesBlock(p0, p1, cell_centers) + ind = get_indices_block(p0, p1, cell_centers) sigma[ind] = top_value @@ -510,58 +509,3 @@ def get_indices_polygon(mesh, pts): hull = Delaunay(pts) inds = hull.find_simplex(mesh.cell_centers) >= 0 return inds - - -################################################ -# DEPRECATED FUNCTIONS -################################################ - - -addBlock = deprecate_function( - add_block, "addBlock", removal_version="0.19.0", future_warn=True -) - -getIndicesBlock = deprecate_function( - get_indices_block, "getIndicesBlock", removal_version="0.19.0", future_warn=True -) - -defineBlock = deprecate_function( - create_block_in_wholespace, - "defineBlock", - removal_version="0.19.0", - future_warn=True, -) - -defineEllipse = deprecate_function( - create_ellipse_in_wholespace, - "defineEllipse", - removal_version="0.19.0", - future_warn=True, -) - -getIndicesSphere = deprecate_function( - get_indices_sphere, "getIndicesSphere", removal_version="0.19.0", future_warn=True -) - -defineTwoLayers = deprecate_function( - create_2_layer_model, "defineTwoLayers", removal_version="0.19.0", future_warn=True -) - -layeredModel = deprecate_function( - create_layers_model, "layeredModel", removal_version="0.19.0", future_warn=True -) - -randomModel = deprecate_function( - create_random_model, "randomModel", removal_version="0.19.0", future_warn=True -) - -polygonInd = deprecate_function( - get_indices_polygon, "polygonInd", removal_version="0.19.0", future_warn=True -) - -scalarConductivity = deprecate_function( - create_from_function, - "scalarConductivity", - removal_version="0.19.0", - future_warn=True, -) diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index c5cd310e66..bb36c19a78 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -14,7 +14,7 @@ def run(plotIt=True): h1 = utils.unpack_widths([(6, 7, -1.5), (6, 10), (6, 7, 1.5)]) h1 = h1 / h1.sum() M2 = discretize.TensorMesh([h1, h1]) - V = utils.model_builder.randomModel(M.vnC, seed=79, its=50) + V = utils.model_builder.create_random_model(M.vnC, seed=79, its=50) v = utils.mkvc(V) modh = maps.Mesh2Mesh([M, M2]) modH = maps.Mesh2Mesh([M2, M]) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index a41d684ceb..270e7cec22 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -72,7 +72,7 @@ def run(plotIt=True): model[mesh.gridCC[:, 0] < 0] = 0.01 # Add a block in half-space - model = utils.model_builder.addBlock( + model = utils.model_builder.add_block( mesh.gridCC, model, np.r_[-10, -10, 20], np.r_[10, 10, 40], 0.05 ) diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index d44988356d..cc8fe41f78 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -115,7 +115,7 @@ # Here a simple block in half-space # Get the indices of the magnetized block model = np.zeros(mesh.nC) -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-10, -10, -30], np.r_[10, 10, -10], mesh.gridCC, diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index c24b4f6b86..9c420650b6 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -131,7 +131,7 @@ M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 1479020d99..bc23e82d3c 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -97,7 +97,7 @@ # model_azm_dip = np.zeros((mesh.nC, 2)) model_amp = np.ones(mesh.nC) * 1e-8 -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-30, -20, -10], np.r_[30, 20, 25], mesh.gridCC, diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index e62cc9ec9f..3f43150103 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -125,7 +125,7 @@ M_xyz = utils.mat_utils.dip_azimuth2cartesian(np.ones(nC) * M[0], np.ones(nC) * M[1]) # Get the indicies of the magnetized block -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index 1143b256dc..42ddce126a 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -40,7 +40,7 @@ def run(plotIt=True): ) # Setup the model conds = [1, 1e-2] - sig = utils.model_builder.defineBlock( + sig = utils.model_builder.create_block_in_wholespace( M.gridCC, [-100, -100, -350], [100, 100, -150], conds ) sig[M.gridCC[:, 2] > 0] = 1e-8 diff --git a/examples/20-published/plot_tomo_joint_with_volume.py b/examples/20-published/plot_tomo_joint_with_volume.py index f9d678ed4f..2b9c445917 100644 --- a/examples/20-published/plot_tomo_joint_with_volume.py +++ b/examples/20-published/plot_tomo_joint_with_volume.py @@ -101,7 +101,7 @@ def run(plotIt=True): # phi model phi0 = 0 phi1 = 0.65 - phitrue = utils.model_builder.defineBlock( + phitrue = utils.model_builder.create_block_in_wholespace( M.gridCC, [0.4, 0.6], [0.6, 0.4], [phi1, phi0] ) diff --git a/examples/_archived/plot_inv_dcip_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_2_5Dinversion.py index 4ea9868683..ba8ba33d03 100644 --- a/examples/_archived/plot_inv_dcip_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_2_5Dinversion.py @@ -64,13 +64,13 @@ def run(plotIt=True, survey_type="dipole-dipole"): survey_dc.drape_electrodes_on_topography(mesh, actind, option="top") # Build conductivity and chargeability model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_charg = utils.model_builder.getIndicesSphere( + blk_inds_charg = utils.model_builder.get_indices_sphere( np.r_[100.0, -25], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index ca907925e4..1caac9b8d2 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -66,10 +66,10 @@ def run(plotIt=True, survey_type="dipole-dipole"): survey.drape_electrodes_on_topography(mesh, actind, option="top") # Build a conductivity model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index 6873c2c38f..188662e72e 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -74,10 +74,10 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): survey.drape_electrodes_on_topography(mesh, actind, option="top") # Build a conductivity model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index f34e210287..e7e5699224 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -71,7 +71,7 @@ def setUp(self): M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 8e9115982d..cf16cb1578 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -83,7 +83,7 @@ def setUp(self): # We can now create a susceptibility model and generate data # Lets start with a simple block in half-space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.gridCC, np.zeros(self.mesh.nC), np.r_[-20, -20, -15], diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index a83216b7ad..0118ac78f9 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -76,7 +76,7 @@ def setUp(self): ) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index ecc2b1eea3..7d465b6d87 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -324,14 +324,14 @@ def setUp(self): # determine comparison locations ROI_large_BNW = np.array([-200, -100]) ROI_large_TSE = np.array([200, 0]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, mesh.gridN )[0] # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-50, -25]) ROI_small_TSE = np.array([50, 0]) - ROI_smallInds = utils.model_builder.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, mesh.gridN )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index acc37e5a59..4257187a29 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -76,14 +76,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid )[0] # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) - ROI_smallInds = utils.model_builder.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid )[0] # print(ROI_smallInds.shape) @@ -249,14 +249,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid )[0] # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) - ROI_smallInds = utils.model_builder.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index 19d8f5eacd..dd725b6d04 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -104,14 +104,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid )[0] # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) - ROI_smallInds = utils.model_builder.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid )[0] # print(ROI_smallInds.shape) @@ -278,14 +278,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid )[0] # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) - ROI_smallInds = utils.model_builder.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_IP_2D_fwd.py b/tests/em/static/test_IP_2D_fwd.py index 10beaf71da..ef416f2594 100644 --- a/tests/em/static/test_IP_2D_fwd.py +++ b/tests/em/static/test_IP_2D_fwd.py @@ -39,7 +39,7 @@ def setUp(self): surveyDC = dc.Survey([src0, src1]) sigmaInf = np.ones(mesh.nC) * 1.0 - blkind = utils.model_builder.getIndicesSphere(np.r_[0, -150], 40, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(np.r_[0, -150], 40, mesh.gridCC) eta = np.zeros(mesh.nC) eta[blkind] = 0.1 @@ -148,7 +148,7 @@ def setUp(self): survey_ip = ip.Survey([src0_ip, src1_ip]) sigmaInf = np.ones(mesh.nC) * 1.0 - blkind = utils.model_builder.getIndicesSphere(np.r_[0, -150], 40, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(np.r_[0, -150], 40, mesh.gridCC) eta = np.zeros(mesh.nC) eta[blkind] = 0.05 diff --git a/tests/em/static/test_IP_fwd.py b/tests/em/static/test_IP_fwd.py index fbc1003180..9f8f6e87d8 100644 --- a/tests/em/static/test_IP_fwd.py +++ b/tests/em/static/test_IP_fwd.py @@ -34,7 +34,7 @@ def setUp(self): N = utils.ndgrid(x + 12.5, y, np.r_[0.0]) radius = 50.0 xc = np.r_[0.0, 0.0, -100] - blkind = utils.model_builder.getIndicesSphere(xc, radius, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(xc, radius, mesh.gridCC) sigmaInf = np.ones(mesh.nC) * 1e-2 eta = np.zeros(mesh.nC) eta[blkind] = 0.1 @@ -131,7 +131,7 @@ def setUp(self): N = utils.ndgrid(x + 12.5, y, np.r_[0.0]) radius = 50.0 xc = np.r_[0.0, 0.0, -100] - blkind = utils.model_builder.getIndicesSphere(xc, radius, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(xc, radius, mesh.gridCC) sigmaInf = np.ones(mesh.nC) * 1e-2 eta = np.zeros(mesh.nC) eta[blkind] = 0.1 diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 153792856a..887aad24db 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -28,10 +28,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) @@ -120,10 +120,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) @@ -211,10 +211,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index dfafb7b83f..00e5370bd6 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -28,10 +28,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 @@ -127,10 +127,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 @@ -224,10 +224,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20), (cs, 0, 1.3)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCC") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 53cc321e63..65793bb650 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -1,7 +1,7 @@ import unittest import discretize from SimPEG import utils, maps -from SimPEG.utils.model_builder import getIndicesSphere +from SimPEG.utils.model_builder import get_indices_sphere from SimPEG.potential_fields import magnetics as mag import numpy as np from pymatsolver import Pardiso @@ -28,7 +28,7 @@ def setUp(self): self.rad = 100 self.sphere_center = [0.0, 0.0, 0.0] - sph_ind = getIndicesSphere(self.sphere_center, self.rad, M.gridCC) + sph_ind = get_indices_sphere(self.sphere_center, self.rad, M.gridCC) chi[sph_ind] = self.chiblk xr = np.linspace(-300, 300, 41) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 8a4bfc27e0..49809e4c7d 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -70,7 +70,7 @@ def setUp(self): M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index cd6e6fd0ba..d30e8a7184 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -81,7 +81,7 @@ def setUp(self): # We can now create a susceptibility model and generate data # Lets start with a simple block in half-space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.gridCC, np.zeros(self.mesh.nC), np.r_[-20, -20, -15], diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index cf7f3615ee..318964328f 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -75,7 +75,7 @@ def setUp(self): ) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index bfd94afc13..84d56ee320 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -70,7 +70,7 @@ def setUp(self): M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, diff --git a/tests/pf/test_magnetics_analytics.py b/tests/pf/test_magnetics_analytics.py index 2711b76398..0a53870aa8 100644 --- a/tests/pf/test_magnetics_analytics.py +++ b/tests/pf/test_magnetics_analytics.py @@ -3,7 +3,7 @@ # from SimPEG import Mesh, PF import discretize from SimPEG.potential_fields import magnetics as mag -from SimPEG.utils.model_builder import getIndicesSphere +from SimPEG.utils.model_builder import get_indices_sphere import numpy as np from scipy.constants import mu_0 @@ -21,7 +21,7 @@ def test_ana_boundary_computation(self): chibkg = 0.0 chiblk = 0.01 chi = np.ones(M3.nC) * chibkg - sph_ind = getIndicesSphere([0, 0, 0], 100, M3.gridCC) + sph_ind = get_indices_sphere([0, 0, 0], 100, M3.gridCC) chi[sph_ind] = chiblk Bbc, const = mag.analytics.CongruousMagBC(M3, np.array([1.0, 0.0, 0.0]), chi) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index e498dfd8a7..78dc40de46 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -316,7 +316,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): # Create a density model and generate data, # with a block in a half space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.cell_centers, np.zeros(self.mesh.nC), np.r_[-20, -20], @@ -324,7 +324,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): 1.0, ) - self.active_cells = utils.model_builder.addBlock( + self.active_cells = utils.model_builder.add_block( self.mesh.cell_centers, np.zeros(self.mesh.nC, dtype=bool), np.r_[-40, -40], diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index e5bbf0d705..99d5fb4f37 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -8,7 +8,7 @@ # #import simpeg.PF as PF # from SimPEG import maps, utils # from SimPEG.potential_fields import magnetics as mag -# from SimPEG.utils.model_builder import getIndicesSphere +# from SimPEG.utils.model_builder import get_indices_sphere # from scipy.constants import mu_0 # # @@ -30,7 +30,7 @@ # H0 = (Btot, Inc, Dec) # # b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) -# sph_ind = getIndicesSphere([0., 0., 0.], 100, M.gridCC) +# sph_ind = get_indices_sphere([0., 0., 0.], 100, M.gridCC) # chi[sph_ind] = chiblk # # xr = np.linspace(-300, 300, 41) diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index 97eff392fb..ae9d8cd16e 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -30,7 +30,7 @@ def setUp(self): self.survey = survey def test_deriv(self): - s = utils.mkvc(utils.model_builder.randomModel(self.M.vnC)) + 1.0 + s = utils.mkvc(utils.model_builder.create_random_model(self.M.vnC)) + 1.0 def fun(x): return self.problem.dpred(x), lambda x: self.problem.Jvec(s, x) diff --git a/tutorials/01-models_mapping/plot_1_tensor_models.py b/tutorials/01-models_mapping/plot_1_tensor_models.py index 6728ba2a1d..939c61aa69 100644 --- a/tutorials/01-models_mapping/plot_1_tensor_models.py +++ b/tutorials/01-models_mapping/plot_1_tensor_models.py @@ -210,7 +210,9 @@ def make_example_mesh(): model = background_value * np.ones(ind_active.sum()) # Add a sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So it's same size and order as model model[ind_sphere] = sphere_value @@ -313,7 +315,9 @@ def make_example_mesh(): model = np.kron(np.ones((N, 1)), np.c_[background_sigma, background_myu]) # Add a conductive and permeable sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere, :] = np.c_[sphere_sigma, sphere_mu] diff --git a/tutorials/01-models_mapping/plot_3_tree_models.py b/tutorials/01-models_mapping/plot_3_tree_models.py index 069e969bb8..2204bcb800 100644 --- a/tutorials/01-models_mapping/plot_3_tree_models.py +++ b/tutorials/01-models_mapping/plot_3_tree_models.py @@ -221,7 +221,9 @@ def refine_box(mesh): model = background_value * np.ones(ind_active.sum()) # Add a sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere] = sphere_value @@ -334,7 +336,9 @@ def refine_box(mesh): model = np.kron(np.ones((N, 1)), np.c_[background_sigma_value, background_mu_value]) # Add a conductive and permeable sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-20.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-20.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere, :] = np.c_[sphere_sigma_value, sphere_mu_value] diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index 41aa7cc1bd..f511e9e2a1 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -138,7 +138,9 @@ model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] model[ind_sphere] = sphere_density diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 5d2c12bc93..06753d7f2d 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -160,7 +160,9 @@ model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] model[ind_sphere] = sphere_density diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 2b3d2fc55d..3b0c46dcfe 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -321,7 +321,9 @@ true_model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] true_model[ind_sphere] = sphere_density diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index a58ae649dc..fdcca19bc8 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -326,7 +326,9 @@ true_model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] true_model[ind_sphere] = sphere_density diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index 42e6cd1b68..75e8b2bca1 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -128,7 +128,7 @@ # Define model. Models in SimPEG are vector arrays model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.getIndicesSphere( +ind_sphere = model_builder.get_indices_sphere( np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers ) ind_sphere = ind_sphere[ind_active] diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index a97cbc9462..be8f7a63b7 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -161,7 +161,7 @@ # Define susceptibility for each cell susceptibility_model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.getIndicesSphere(np.r_[0.0, 0.0, -45.0], 15.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere(np.r_[0.0, 0.0, -45.0], 15.0, mesh.gridCC) ind_sphere = ind_sphere[ind_active] susceptibility_model[ind_sphere] = sphere_susceptibility diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 69fc1932f0..0b4bb43ec7 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -345,7 +345,7 @@ sphere_susceptibility = 0.01 true_model = background_susceptibility * np.ones(nC) -ind_sphere = model_builder.getIndicesSphere( +ind_sphere = model_builder.get_indices_sphere( np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers ) ind_sphere = ind_sphere[active_cells] diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 259b5b6137..2e7935463b 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -183,11 +183,13 @@ # Define model conductivity_model = background_conductivity * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -160.0], 60.0, mesh.gridCC) +ind_conductor = model_builder.get_indices_sphere( + np.r_[-120.0, -160.0], 60.0, mesh.gridCC +) ind_conductor = ind_conductor[ind_active] conductivity_model[ind_conductor] = conductor_conductivity -ind_resistor = model_builder.getIndicesSphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) +ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) ind_resistor = ind_resistor[ind_active] conductivity_model[ind_resistor] = resistor_conductivity diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index ea9064813c..e0b7f377a2 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -190,12 +190,12 @@ # Define model conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +ind_conductor = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) conductivity_model[ind_conductor] = conductor_value -ind_resistor = model_builder.getIndicesSphere( +ind_resistor = model_builder.get_indices_sphere( np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) conductivity_model[ind_resistor] = resistor_value diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index 1ec103157f..a0479907ad 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -369,10 +369,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +ind_conductor = model_builder.get_indices_sphere( + np.r_[-120.0, -180.0], 60.0, mesh.gridCC +) true_conductivity_model[ind_conductor] = true_conductor_conductivity -ind_resistor = model_builder.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity true_conductivity_model[~ind_active] = np.NaN diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index e30b7e0e77..2cf97a0bc8 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -385,10 +385,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +ind_conductor = model_builder.get_indices_sphere( + np.r_[-120.0, -180.0], 60.0, mesh.gridCC +) true_conductivity_model[ind_conductor] = true_conductor_conductivity -ind_resistor = model_builder.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity true_conductivity_model[~ind_active] = np.NaN diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 3ff0ad4676..16498c2aad 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -388,12 +388,12 @@ # Define model true_conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +ind_conductor = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_conductivity_model[ind_conductor] = conductor_value -ind_resistor = model_builder.getIndicesSphere( +ind_resistor = model_builder.get_indices_sphere( np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_conductivity_model[ind_resistor] = resistor_value diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index eb7bac5aa6..0631392f6d 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -195,11 +195,13 @@ # Define model conductivity_model = background_conductivity * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -160.0], 60.0, mesh.gridCC) +ind_conductor = model_builder.get_indices_sphere( + np.r_[-120.0, -160.0], 60.0, mesh.gridCC +) ind_conductor = ind_conductor[ind_active] conductivity_model[ind_conductor] = conductor_conductivity -ind_resistor = model_builder.getIndicesSphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) +ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) ind_resistor = ind_resistor[ind_active] conductivity_model[ind_resistor] = resistor_conductivity @@ -353,7 +355,7 @@ # Define chargeability model chargeability_model = background_chargeability * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-120.0, -160.0], 60.0, mesh.gridCC ) ind_chargeable = ind_chargeable[ind_active] diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index d7d9aebd78..3c522e531c 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -196,12 +196,12 @@ # Define model conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +ind_conductor = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) conductivity_model[ind_conductor] = conductor_value -ind_resistor = model_builder.getIndicesSphere( +ind_resistor = model_builder.get_indices_sphere( np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) conductivity_model[ind_resistor] = resistor_value @@ -388,7 +388,7 @@ # Define model chargeability_model = background_value * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index f34d6996b2..2215462ffd 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -393,10 +393,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +ind_conductor = model_builder.get_indices_sphere( + np.r_[-120.0, -180.0], 60.0, mesh.gridCC +) true_conductivity_model[ind_conductor] = true_conductor_conductivity -ind_resistor = model_builder.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity true_conductivity_model[~ind_active] = np.NaN diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index fa4fb33440..9193d829b7 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -430,12 +430,12 @@ resistor_value = 1e-3 true_conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +ind_conductor = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_conductivity_model[ind_conductor] = conductor_value -ind_resistor = model_builder.getIndicesSphere( +ind_resistor = model_builder.get_indices_sphere( np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_conductivity_model[ind_resistor] = resistor_value @@ -672,7 +672,7 @@ chargeable_value = 1e-1 true_chargeability_model = background_value * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_chargeability_model[ind_chargeable] = chargeable_value diff --git a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py index 083b653af2..1f16d0b4cd 100644 --- a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py @@ -94,7 +94,7 @@ # Define the model. Models in SimPEG are vector arrays. model = background_velocity * np.ones(mesh.nC) -ind_block = model_builder.getIndicesBlock(np.r_[-50, 20], np.r_[50, -20], mesh.gridCC) +ind_block = model_builder.get_indices_block(np.r_[-50, 20], np.r_[50, -20], mesh.gridCC) model[ind_block] = block_velocity # Define a mapping from the model (velocity) to the slowness. If your model From 5231446ee858fe677cdcbdcfe65be081b61ebd20 Mon Sep 17 00:00:00 2001 From: Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:21:18 -0800 Subject: [PATCH 318/455] Triaxial magnetic gradient forward modelling (#1288) This PR adds magnetic forward modelling for the gradient of the TMI, measured when several magnetometers are mounted on the same aircraft. A good example is [the Heli-GT system from SHA geophysics ](https://www.shageophysics.com/#js__scroll-to-section_heli-gt) but many airborne systems have several magnetometers mounted. --------- Co-authored-by: Santiago Soler --- .../potential_fields/magnetics/receivers.py | 6 + .../potential_fields/magnetics/simulation.py | 102 ++++++++---- tests/pf/test_forward_Mag_Linear.py | 146 ++++++++++++++++-- 3 files changed, 214 insertions(+), 40 deletions(-) diff --git a/SimPEG/potential_fields/magnetics/receivers.py b/SimPEG/potential_fields/magnetics/receivers.py index 8a290959c1..6f885ad635 100644 --- a/SimPEG/potential_fields/magnetics/receivers.py +++ b/SimPEG/potential_fields/magnetics/receivers.py @@ -23,6 +23,9 @@ class Point(survey.BaseRx): - "byy" --> y-derivative of the y-component - "byz" --> z-derivative of the y-component (and visa versa) - "bzz" --> z-derivative of the z-component + - "tmi_x"--> x-derivative of the total magnetic intensity data + - "tmi_y"--> y-derivative of the total magnetic intensity data + - "tmi_z"--> z-derivative of the total magnetic intensity data Notes ----- @@ -51,6 +54,9 @@ def __init__(self, locations, components="tmi", **kwargs): "by", "bz", "tmi", + "tmi_x", + "tmi_y", + "tmi_z", ], ) self.components = components diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/SimPEG/potential_fields/magnetics/simulation.py index 8d926d5e4a..11fdfb5a70 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/SimPEG/potential_fields/magnetics/simulation.py @@ -1,27 +1,24 @@ import numpy as np import scipy.sparse as sp -from scipy.constants import mu_0 - -from SimPEG import utils -from ..base import BasePFSimulation, BaseEquivalentSourceLayerSimulation -from ...base import BaseMagneticPDESimulation -from .survey import Survey -from .analytics import CongruousMagBC - -from SimPEG import Solver -from SimPEG import props - -from SimPEG.utils import mkvc, mat_utils, sdiag -from SimPEG.utils.code_utils import validate_string, deprecate_property, validate_type from geoana.kernels import ( - prism_fzz, - prism_fzx, - prism_fzy, - prism_fzzz, prism_fxxy, prism_fxxz, prism_fxyz, + prism_fzx, + prism_fzy, + prism_fzz, + prism_fzzz, ) +from scipy.constants import mu_0 + +from SimPEG import Solver, props, utils +from SimPEG.utils import mat_utils, mkvc, sdiag +from SimPEG.utils.code_utils import deprecate_property, validate_string, validate_type + +from ...base import BaseMagneticPDESimulation +from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation +from .analytics import CongruousMagBC +from .survey import Survey class Simulation3DIntegral(BasePFSimulation): @@ -246,7 +243,7 @@ def evaluate_integral(self, receiver_location, components): components: list[str] List of magnetic components chosen from: - 'bx', 'by', 'bz', 'bxx', 'bxy', 'bxz', 'byy', 'byz', 'bzz' + 'tmi', 'bx', 'by', 'bz', 'bxx', 'bxy', 'bxz', 'byy', 'byz', 'bzz', 'tmi_x', 'tmi_y', 'tmi_z' OUTPUT: Tx = [Txx Txy Txz] @@ -283,33 +280,33 @@ def evaluate_integral(self, receiver_location, components): # # inside an active cell. # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] - if "bxx" in components: + if "bxx" in components or "tmi_x" in components: node_evals["gxxx"] = prism_fzzz(dy, dz, dx) node_evals["gxxy"] = prism_fxxy(dx, dy, dz) node_evals["gxxz"] = prism_fxxz(dx, dy, dz) - if "bxy" in components: + if "bxy" in components or "tmi_x" in components or "tmi_y" in components: if "gxxy" not in node_evals: node_evals["gxxy"] = prism_fxxy(dx, dy, dz) node_evals["gyyx"] = prism_fxxz(dy, dz, dx) node_evals["gxyz"] = prism_fxyz(dx, dy, dz) - if "bxz" in components: + if "bxz" in components or "tmi_x" in components or "tmi_z" in components: if "gxxz" not in node_evals: node_evals["gxxz"] = prism_fxxz(dx, dy, dz) if "gxyz" not in node_evals: node_evals["gxyz"] = prism_fxyz(dx, dy, dz) node_evals["gzzx"] = prism_fxxy(dz, dx, dy) - if "byy" in components: + if "byy" in components or "tmi_y" in components: if "gyyx" not in node_evals: node_evals["gyyx"] = prism_fxxz(dy, dz, dx) node_evals["gyyy"] = prism_fzzz(dz, dx, dy) node_evals["gyyz"] = prism_fxxy(dy, dz, dx) - if "byz" in components: + if "byz" in components or "tmi_y" in components or "tmi_z" in components: if "gxyz" not in node_evals: node_evals["gxyz"] = prism_fxyz(dx, dy, dz) if "gyyz" not in node_evals: node_evals["gyyz"] = prism_fxxy(dy, dz, dx) node_evals["gzzy"] = prism_fxxz(dz, dx, dy) - if "bzz" in components: + if "bzz" in components or "tmi_z" in components: if "gzzx" not in node_evals: node_evals["gzzx"] = prism_fxxy(dz, dx, dy) if "gzzy" not in node_evals: @@ -355,6 +352,57 @@ def evaluate_integral(self, receiver_location, components): + tmi[1] * node_evals["gyz"] + tmi[2] * node_evals["gzz"] ) + elif component == "tmi_x": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxx"] + + tmi[1] * node_evals["gxxy"] + + tmi[2] * node_evals["gxxz"] + ) + vals_y = ( + tmi[0] * node_evals["gxxy"] + + tmi[1] * node_evals["gyyx"] + + tmi[2] * node_evals["gxyz"] + ) + vals_z = ( + tmi[0] * node_evals["gxxz"] + + tmi[1] * node_evals["gxyz"] + + tmi[2] * node_evals["gzzx"] + ) + elif component == "tmi_y": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxy"] + + tmi[1] * node_evals["gyyx"] + + tmi[2] * node_evals["gxyz"] + ) + vals_y = ( + tmi[0] * node_evals["gyyx"] + + tmi[1] * node_evals["gyyy"] + + tmi[2] * node_evals["gyyz"] + ) + vals_z = ( + tmi[0] * node_evals["gxyz"] + + tmi[1] * node_evals["gyyz"] + + tmi[2] * node_evals["gzzy"] + ) + elif component == "tmi_z": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxz"] + + tmi[1] * node_evals["gxyz"] + + tmi[2] * node_evals["gzzx"] + ) + vals_y = ( + tmi[0] * node_evals["gxyz"] + + tmi[1] * node_evals["gyyz"] + + tmi[2] * node_evals["gzzy"] + ) + vals_z = ( + tmi[0] * node_evals["gzzx"] + + tmi[1] * node_evals["gzzy"] + + tmi[2] * node_evals["gzzz"] + ) elif component == "bxx": vals_x = node_evals["gxxx"] vals_y = node_evals["gxxy"] @@ -949,11 +997,11 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): """ from SimPEG import ( - optimization, - regularization, directives, - objective_function, inversion, + objective_function, + optimization, + regularization, ) prob = Simulation3DDifferential(mesh, survey=data, mu=model) diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 3f412651db..662556b93e 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,10 +1,12 @@ import unittest + import discretize -from SimPEG import utils, maps -from SimPEG.potential_fields import magnetics as mag +import numpy as np from geoana.em.static import MagneticPrism from scipy.constants import mu_0 -import numpy as np + +from SimPEG import maps, utils +from SimPEG.potential_fields import magnetics as mag def test_ana_mag_forward(): @@ -47,7 +49,7 @@ def get_block_inds(grid, block): active_cells = model != 0.0 model_reduced = model[active_cells] - # Create reduced identity map for Linear Pproblem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) # Create plane of observations @@ -59,10 +61,12 @@ def get_block_inds(grid, block): components = ["bx", "by", "bz", "tmi"] rxLoc = mag.Point(locXyz, components=components) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + ) survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Pproblem + # Creat reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) sim = mag.Simulation3DIntegral( @@ -101,6 +105,116 @@ def get_block_inds(grid, block): np.testing.assert_allclose(d_t, d @ tmi) +def test_ana_mag_tmi_grad_forward(): + nx = 61 + ny = 61 + + H0 = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + chi1 = 0.01 + chi2 = 0.02 + + # Define a mesh + cs = 0.2 + hxind = [(cs, 41)] + hyind = [(cs, 41)] + hzind = [(cs, 41)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + + # create a model of two blocks, 1 inside the other + block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) + block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) + + def get_block_inds(grid, block): + return np.where( + (grid[:, 0] > block[0, 0]) + & (grid[:, 0] < block[0, 1]) + & (grid[:, 1] > block[1, 0]) + & (grid[:, 1] < block[1, 1]) + & (grid[:, 2] > block[2, 0]) + & (grid[:, 2] < block[2, 1]) + ) + + block1_inds = get_block_inds(mesh.cell_centers, block1) + block2_inds = get_block_inds(mesh.cell_centers, block2) + + model = np.zeros(mesh.n_cells) + model[block1_inds] = chi1 + model[block2_inds] = chi2 + + active_cells = model != 0.0 + model_reduced = model[active_cells] + + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + + # Create plane of observations + xr = np.linspace(-20, 20, nx) + dxr = xr[1] - xr[0] + yr = np.linspace(-20, 20, ny) + dyr = yr[1] - yr[0] + X, Y = np.meshgrid(xr, yr) + Z = np.ones_like(X) * 3.0 + locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] + components = ["tmi", "tmi_x", "tmi_y", "tmi_z"] + + rxLoc = mag.Point(locXyz, components=components) + srcField = mag.UniformBackgroundField( + [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + ) + survey = mag.Survey(srcField) + + # Creat reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + + sim = mag.Simulation3DIntegral( + mesh, + survey=survey, + chiMap=idenMap, + ind_active=active_cells, + store_sensitivities="forward_only", + n_processes=None, + ) + + data = sim.dpred(model_reduced) + tmi = data[0::4] + d_x = data[1::4] + d_y = data[2::4] + d_z = data[3::4] + + # Compute analytical response from magnetic prism + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(locXyz) + + prism_2.magnetic_field_gradient(locXyz) + + prism_3.magnetic_field_gradient(locXyz) + ) * mu_0 + tmi_x = (d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2]) / H0[0] + tmi_y = (d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2]) / H0[0] + tmi_z = (d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2]) / H0[0] + np.testing.assert_allclose(d_x, tmi_x, rtol=1e-10, atol=1e-12) + np.testing.assert_allclose(d_y, tmi_y, rtol=1e-10, atol=1e-12) + np.testing.assert_allclose(d_z, tmi_z, rtol=1e-10, atol=1e-12) + + # finite difference test y-grad + np.testing.assert_allclose( + np.diff(tmi.reshape(nx, ny, order="F")[:, ::2], axis=1) / (2 * dyr), + tmi_y.reshape(nx, ny, order="F")[:, 1::2], + atol=1.0, + rtol=1e-1, + ) + # finite difference test x-grad + np.testing.assert_allclose( + np.diff(tmi.reshape(nx, ny, order="F")[::2, :], axis=0) / (2 * dxr), + tmi_x.reshape(nx, ny, order="F")[1::2, :], + atol=1.0, + rtol=1e-1, + ) + + def test_ana_mag_grad_forward(): nx = 5 ny = 5 @@ -141,7 +255,7 @@ def get_block_inds(grid, block): active_cells = model != 0.0 model_reduced = model[active_cells] - # Create reduced identity map for Linear Pproblem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) # Create plane of observations @@ -153,10 +267,12 @@ def get_block_inds(grid, block): components = ["bxx", "bxy", "bxz", "byy", "byz", "bzz"] rxLoc = mag.Point(locXyz, components=components) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + ) survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Pproblem + # Creat reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) sim = mag.Simulation3DIntegral( @@ -245,10 +361,12 @@ def get_block_inds(grid, block): components = ["bx", "by", "bz", "tmi"] rxLoc = mag.Point(locXyz, components=components) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + ) survey = mag.Survey(srcField) - # Create reduced identity map for Linear Pproblem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) sim = mag.Simulation3DIntegral( @@ -331,10 +449,12 @@ def get_block_inds(grid, block): components = ["bx", "by", "bz"] rxLoc = mag.Point(locXyz, components=components) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + ) survey = mag.Survey(srcField) - # Create reduced identity map for Linear Pproblem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) sim = mag.Simulation3DIntegral( From 51fcf3c0468ad24a0542d3370bf0dac987d62d39 Mon Sep 17 00:00:00 2001 From: "Williams A. Lima" Date: Thu, 11 Jan 2024 15:09:32 -0300 Subject: [PATCH 319/455] Documentation improvements for classes in Objective Function Pieces (#1292) Add proper docstrings to classes and methods in `data_misfit.py` and `objective_function.py`. --------- Co-authored-by: Devin C. Cowan Co-authored-by: Santiago Soler --- SimPEG/data_misfit.py | 191 +++++++++++++++++----- SimPEG/objective_function.py | 308 ++++++++++++++++++++++++----------- SimPEG/simulation.py | 4 +- 3 files changed, 370 insertions(+), 133 deletions(-) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py index c111e42555..42ffc6532d 100644 --- a/SimPEG/data_misfit.py +++ b/SimPEG/data_misfit.py @@ -8,12 +8,32 @@ class BaseDataMisfit(L2ObjectiveFunction): - """ - BaseDataMisfit + r"""Base data misfit class. + + Inherit this class to build your own data misfit function. The ``BaseDataMisfit`` + class inherits the :py:class:`SimPEG.objective_function.L2ObjectiveFunction`. + And as a result, it is limited to building data misfit functions of the form: + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own data misfit class. - .. note:: - You should inherit from this class to create your own data misfit - term. + .. math:: + \phi_d (\mathbf{m}) = \frac{1}{2} \| \mathbf{W} f(\mathbf{m}) \|_2^2 + + where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{W}` is a linear weighting + matrix, and :math:`f` is a mapping function that acts on the model. + + Parameters + ---------- + data : SimPEG.data.Data + A SimPEG data object. + simulation : SimPEG.simulation.BaseSimulation + A SimPEG simulation object. + debug : bool + Print debugging information. + counter : None or SimPEG.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. """ def __init__(self, data, simulation, debug=False, counter=None, **kwargs): @@ -24,11 +44,12 @@ def __init__(self, data, simulation, debug=False, counter=None, **kwargs): @property def data(self): - """A SimPEG data class containing the observed data. + """A SimPEG data object. Returns ------- SimPEG.data.Data + A SimPEG data object. """ return self._data @@ -38,11 +59,12 @@ def data(self, value): @property def simulation(self): - """A SimPEG simulation. + """A SimPEG simulation object. Returns ------- SimPEG.simulation.BaseSimulation + A SimPEG simulation object. """ return self._simulation @@ -59,6 +81,7 @@ def debug(self): Returns ------- bool + Print debugging information. """ return self._debug @@ -68,11 +91,12 @@ def debug(self, value): @property def counter(self): - """Set this to a ``SimPEG.utils.Counter`` if you want to count things. + """SimPEG ``Counter`` object to store iterations and run-times. Returns ------- - SimPEG.utils.Counter or None + None or SimPEG.utils.Counter + SimPEG ``Counter`` object to store iterations and run-times. """ return self._counter @@ -83,8 +107,12 @@ def counter(self, value): @property def nP(self): - """ - number of model parameters + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. """ if self._mapping is not None: return self.mapping.nP @@ -95,23 +123,44 @@ def nP(self): @property def nD(self): - """ - number of data + """Number of data. + + Returns + ------- + int + Number of data. """ return self.data.nD @property def shape(self): - """""" + """Shape of the Jacobian. + + The number of data by the number of model parameters. + + Returns + ------- + tuple of int (n_data, n_param) + Shape of the Jacobian; i.e. number of data by the number of model parameters. + """ return (self.nD, self.nP) @property def W(self): - """W - The data weighting matrix. - The default is based on the norm of the data plus a noise floor. - :rtype: scipy.sparse.csr_matrix - :return: W + r"""The data weighting matrix. + + For a discrete least-squares data misfit function of the form: + + .. math:: + \phi_d (\mathbf{m}) = \frac{1}{2} \| \mathbf{W} \mathbf{f}(\mathbf{m}) \|_2^2 + + :math:`\mathbf{W}` is a linear weighting matrix, :math:`\mathbf{m}` is the model vector, + and :math:`\mathbf{f}` is a discrete mapping function that acts on the model vector. + + Returns + ------- + scipy.sparse.csr_matrix + The data weighting matrix. """ if getattr(self, "_W", None) is None: @@ -154,43 +203,91 @@ def W(self, value): self._W = value def residual(self, m, f=None): + r"""Computes the data residual vector for a given model. + + Where :math:`\mathbf{d}_\text{obs}` is the observed data vector and :math:`\mathbf{d}_\text{pred}` + is the predicted data vector for a model vector :math:`\mathbf{m}`, this function + computes the data residual: + + .. math:: + \mathbf{r} = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + f : None or SimPEG.fields.Fields, optional + A SimPEG fields object. Used when the fields for the model *m* have + already been computed. + + Returns + ------- + (n_data, ) numpy.ndarray + The data residual vector. + """ if self.data is None: raise Exception("data must be set before a residual can be calculated.") return self.simulation.residual(m, self.data.dobs, f=f) class L2DataMisfit(BaseDataMisfit): - r""" - The data misfit with an l_2 norm: + r"""Least-squares data misfit. + + Define the data misfit as the L2-norm of the weighted residual between observed + data and predicted data for a given model. I.e.: .. math:: + \phi_d (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W_d} + \big ( \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} \big ) \big \|_2^2 - \mu_\text{data} = - \frac{1}{2} - \left| - \mathbf{W}_d (\mathbf{d}_\text{pred} - \mathbf{d}_\text{obs}) - \right|_2^2 + where :math:`\mathbf{d}_\text{obs}` is the observed data vector, :math:`\mathbf{d}_\text{pred}` + is the predicted data vector for a model vector :math:`\mathbf{m}`, and + :math:`\mathbf{W_d}` is the data weighting matrix. The diagonal elements of + :math:`\mathbf{W_d}` are the reciprocals of the data uncertainties + :math:`\boldsymbol{\varepsilon}`. Thus: + + .. math:: + \mathbf{W_d} = \text{diag} \left ( \boldsymbol{\varepsilon}^{-1} \right ) + + Parameters + ---------- + data : SimPEG.data.Data + A SimPEG data object that has observed data and uncertainties. + simulation : SimPEG.simulation.BaseSimulation + A SimPEG simulation object. + debug : bool + Print debugging information. + counter : None or SimPEG.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. """ @timeIt def __call__(self, m, f=None): - "__call__(m, f=None)" + """Evaluate the residual for a given model.""" R = self.W * self.residual(m, f=f) return 0.5 * np.vdot(R, R) @timeIt def deriv(self, m, f=None): - r""" - Derivative of the data misfit + r"""Gradient of the data misfit function evaluated for the model provided. + + Where :math:`\phi_d (\mathbf{m})` is the data misfit function, + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: .. math:: + \frac{\partial \phi_d}{\partial \mathbf{m}} - \mathbf{J}^{\top} \mathbf{W}^{\top} \mathbf{W} - (\mathbf{d} - \mathbf{d}^{obs}) + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. - :param numpy.ndarray m: model - :param SimPEG.fields.Fields f: fields object + Returns + ------- + (n_param, ) numpy.ndarray + The gradient of the data misfit function evaluated for the model provided. """ if f is None: @@ -202,16 +299,32 @@ def deriv(self, m, f=None): @timeIt def deriv2(self, m, v, f=None): - r""" - Second derivative of the data misfit + r"""Hessian of the data misfit function evaluated for the model provided. + + Where :math:`\phi_d (\mathbf{m})` is the data misfit function, + this method returns the second-derivative (Hessian) with respect to the model parameters: .. math:: + \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} - \mathbf{J}^{\top} \mathbf{W}^{\top} \mathbf{W} \mathbf{J} + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: - :param numpy.ndarray m: model - :param numpy.ndarray v: vector - :param SimPEG.fields.Fields f: fields object + .. math:: + \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None or (n_param, ) numpy.ndarray, optional + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix or (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the data misfit + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. """ if f is None: diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 400d94d2c5..b25ccb4c4a 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -11,17 +11,39 @@ class BaseObjectiveFunction(BaseSimPEG): - """ - Base Objective Function + """Base class for creating objective functions. + + The ``BaseObjectiveFunction`` class defines properties and methods inherited by + other classes in SimPEG that represent objective functions; e.g. regularization, data misfit. + These include convenient methods for testing the order of convergence and ajoint operations. + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own objective function class. - Inherit this to build your own objective function. If building a - regularization, have a look at - :class:`SimPEG.regularization.BaseRegularization` as there are additional - methods and properties tailored to regularization of a model. Similarly, - for building a data misfit, see :class:`SimPEG.DataMisfit.BaseDataMisfit`. + .. important:: + If building a regularization function within SimPEG, please inherit + :py:class:`SimPEG.regularization.BaseRegularization`, as this class + has additional functionality related to regularization. And if building a data misfit + function, please inherit :py:class:`SimPEG.data_misfit.BaseDataMisfit`. + + Parameters + ---------- + nP : int + Number of model parameters. + mapping : SimPEG.mapping.BaseMap + A SimPEG mapping object that maps from the model space to the + quantity evaluated in the objective function. + has_fields : bool + If ``True``, predicted fields for a simulation and a given model can be + used to evaluate the objective function quickly. + counter : None or SimPEG.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + debug : bool + Print debugging information. """ - map_class = IdentityMap #: Base class of expected maps + map_class = IdentityMap #: Base class of expected maps. def __init__( self, @@ -41,8 +63,15 @@ def __init__( self.has_fields = has_fields def __call__(self, x, f=None): - """ - Evaluate the objective functions for a given model + """Evaluate the objective function for a given model. + + Parameters + ---------- + x : (nP) numpy.ndarray + A vector representing a set of model parameters. + f : SimPEG.fields.Fields, optional + Field object (if applicable). + """ raise NotImplementedError( "__call__ has not been implemented for {} yet".format( @@ -52,8 +81,12 @@ def __call__(self, x, f=None): @property def nP(self): - """ - Number of model parameters expected. + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. """ if self._nP is not None: return self._nP @@ -63,9 +96,7 @@ def nP(self): @property def _nC_residual(self): - """ - Shape of the residual - """ + """Shape of the residual.""" if getattr(self, "mapping", None) is not None: return self.mapping.shape[0] else: @@ -73,8 +104,12 @@ def _nC_residual(self): @property def mapping(self): - """ - A `SimPEG.Maps` instance + """Mapping from the model to the quantity evaluated in the object function. + + Returns + ------- + SimPEG.mapping.BaseMap + The mapping from the model to the quantity evaluated in the object function. """ if self._mapping is None: if self._nP is not None: @@ -93,9 +128,25 @@ def mapping(self, value): self._mapping = value @timeIt - def deriv(self, x, **kwargs): - """ - First derivative of the objective function with respect to the model + def deriv(self, m, **kwargs): + r"""Gradient of the objective function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the objective function, + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The gradient of the objective function evaluated for the model provided. """ raise NotImplementedError( "The method deriv has not been implemented for {}".format( @@ -104,9 +155,33 @@ def deriv(self, x, **kwargs): ) @timeIt - def deriv2(self, x, v=None, **kwargs): - """ - Second derivative of the objective function with respect to the model + def deriv2(self, m, v=None, **kwargs): + r"""Hessian of the objective function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the objective function, + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None or (n_param, ) numpy.ndarray, optional + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix or (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the objective + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. """ raise NotImplementedError( "The method _deriv2 has not been implemented for {}".format( @@ -146,9 +221,22 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): ) def test(self, x=None, num=4, **kwargs): - """ - Run a convergence test on both the first and second derivatives - they - should be second order! + """Run a convergence test on both the first and second derivatives. + + They should be second order! + + Parameters + ---------- + x : None or (n_param, ) numpy.ndarray, optional + The evaluation point for the Taylor expansion. + num : int + The number of iterations in the convergence test. + + Returns + ------- + bool + ``True`` if both tests pass. ``False`` if either test fails. + """ deriv = self._test_deriv(x=x, num=num, **kwargs) deriv2 = self._test_deriv2(x=x, num=num, plotIt=False, **kwargs) @@ -197,24 +285,26 @@ def __rdiv__(self, denominator): class ComboObjectiveFunction(BaseObjectiveFunction): - """ - Composite for multiple objective functions + r"""Composite for multiple objective functions. - A composite class for multiple objective functions. Each objective function - is accompanied by a multiplier. Both objective functions and multipliers - are stored in a list. + This class allows the creation of an objective function :math:`\phi` which is the sum + of a list of other objective functions :math:`\phi_i`. Each objective function has associated with it + a multiplier :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i Parameters ---------- - objfcts : list or None, optional + objfcts : None or list of SimPEG.objective_function.BaseObjectiveFunction, optional List containing the objective functions that will live inside the composite class. If ``None``, an empty list will be created. - multipliers : list or None, optional - List containing the multipliers for its respective objective function + multipliers : None or list of int, optional + List containing the multipliers for each objective function in ``objfcts``. If ``None``, a list full of ones with the same length as ``objfcts`` will be created. - unpack_on_add : bool, optional - Weather to unpack the multiple objective functions when adding them to + unpack_on_add : bool + Whether to unpack the multiple objective functions when adding them to another objective function, or to add them as a whole. Examples @@ -265,6 +355,7 @@ class ComboObjectiveFunction(BaseObjectiveFunction): >>> combo_2 = combo_1 + objective_fun_c >>> print(len(combo_2)) 2 + """ _multiplier_types = (float, None, Zero, np.float64, int, np.integer) @@ -301,24 +392,33 @@ def __getitem__(self, key): @property def multipliers(self): - """ - Multipliers for each objective function + r"""Multipliers for the objective functions. + + For a composite objective function :math:`\phi`, that is, a weighted sum of + objective functions :math:`\phi_i` with multipliers :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i, + + this method returns the multipliers :math:`c_i` in + the same order of the ``objfcts``. + + Returns + ------- + list of int + Multipliers for the objective functions. """ return self._multipliers @multipliers.setter def multipliers(self, value): - """ - Set multipliers attribute after checking if they are valid - """ + """Set multipliers attribute after checking if they are valid.""" self._validate_multipliers(value) self._check_length_objective_funcs_multipliers(self.objfcts, value) self._multipliers = value def __call__(self, m, f=None): - """ - Evaluate the objective functions for a given model - """ + """Evaluate the objective functions for a given model.""" fct = 0.0 for i, phi in enumerate(self): multiplier, objfct = phi @@ -332,14 +432,7 @@ def __call__(self, m, f=None): return fct def deriv(self, m, f=None): - """ - First derivative of the composite objective function is the sum of the - derivatives of each objective function in the list, weighted by their - respective multplier. - - :param numpy.ndarray m: model - :param SimPEG.Fields f: Fields object (if applicable) - """ + # Docstring inherited from BaseObjectiveFunction g = Zero() for i, phi in enumerate(self): multiplier, objfct = phi @@ -354,15 +447,7 @@ def deriv(self, m, f=None): return g def deriv2(self, m, v=None, f=None): - """ - Second derivative of the composite objective function is the sum of the - second derivatives of each objective function in the list, weighted by - their respective multplier. - - :param numpy.ndarray m: model - :param numpy.ndarray v: vector we are multiplying by - :param SimPEG.Fields f: Fields object (if applicable) - """ + # Docstring inherited from BaseObjectiveFunction H = Zero() for i, phi in enumerate(self): multiplier, objfct = phi @@ -379,9 +464,27 @@ def deriv2(self, m, v=None, f=None): # The base class currently does not. @property def W(self): - """ - W matrix for the full objective function. Includes multiplying by the - square root of alpha. + r"""Full weighting matrix for the combo objective function. + + Consider a composite objective function :math`\phi` that is a weighted sum of + objective functions :math:`\phi_i` with multipliers :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i = \sum_{i = 1}^N \frac{c_i}{2} + \big \| \mathbf{W}_i \, f_i (\mathbf{m}) \big \|^2_2 + + Where each objective function :math:`\phi_i` has a weighting matrix :math:`W_i`, + this method returns the full weighting matrix for the composite objective function: + + .. math:: + \mathbf{W} = \begin{bmatrix} + \sqrt{c_1} W_i \\ \vdots \\ \sqrt{c_N} W_N + \end{bmatrix} + + Returns + ------- + scipy.sparse.csr_matrix + Full weighting matrix for the combo objective function. """ W = [] for mult, fct in self: @@ -391,8 +494,17 @@ def W(self): return sp.vstack(W) def get_functions_of_type(self, fun_class) -> list: - """ - Find an objective function type from a ComboObjectiveFunction class. + """Return objective functions of a given type(s). + + Parameters + ---------- + fun_class : list or SimPEG.objective_function.BaseObjectiveFunction + Objective function class or list of objective function classes to return. + + Returns + ------- + list of SimPEG.objective_function.BaseObjectiveFunction + Objective functions of a given type(s). """ target = [] if isinstance(self, fun_class): @@ -407,11 +519,11 @@ def get_functions_of_type(self, fun_class) -> list: return [fun for fun in target if fun] def _validate_objective_functions(self, objective_functions): - """ - Validate objective functions + """Validate objective functions. Check if the objective functions have the right types, and if they all have the same number of parameters. + """ for function in objective_functions: if not isinstance(function, BaseObjectiveFunction): @@ -432,10 +544,10 @@ def _validate_objective_functions(self, objective_functions): ) def _validate_multipliers(self, multipliers): - """ - Validate multipliers + """Validate multipliers. Check if the multipliers have the right types. + """ for multiplier in multipliers: if type(multiplier) not in self._multiplier_types: @@ -448,9 +560,7 @@ def _validate_multipliers(self, multipliers): def _check_length_objective_funcs_multipliers( self, objective_functions, multipliers ): - """ - Check if objective functions and multipliers have the same length - """ + """Check if objective functions and multipliers have the same length.""" if len(objective_functions) != len(multipliers): raise ValueError( "Inconsistent number of elements between objective functions " @@ -460,12 +570,33 @@ def _check_length_objective_funcs_multipliers( class L2ObjectiveFunction(BaseObjectiveFunction): - r""" - An L2-Objective Function + r"""Weighted least-squares objective function class. + + Weighting least-squares objective functions in SimPEG are defined as follows: .. math:: + \phi = \frac{1}{2} \big \| \mathbf{W} f(\mathbf{m}) \big \|_2^2 + + where :math:`\mathbf{m}` are the model parameters, :math:`f` is a mapping operator, + and :math:`\mathbf{W}` is the weighting matrix. - \phi = \frac{1}{2}||\mathbf{W} \mathbf{m}||^2 + Parameters + ---------- + nP : int + Number of model parameters. + mapping : SimPEG.mapping.BaseMap + A SimPEG mapping object that maps from the model space to the + quantity evaluated in the objective function. + W : None or scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. By default, this + is set to the identity matrix. + has_fields : bool + If ``True``, predicted fields for a simulation and a given model can be + used to evaluate the objective function quickly. + counter : None or SimPEG.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + debug : bool + Print debugging information. """ def __init__( @@ -496,8 +627,12 @@ def __init__( @property def W(self): - """ - Weighting matrix. The default if not specified is an identity. + """Weighting matrix applied in the objective function. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. """ if getattr(self, "_W", None) is None: if self._nC_residual != "*": @@ -507,27 +642,16 @@ def W(self): return self._W def __call__(self, m): - """ - Evaluate the objective functions for a given model - """ + """Evaluate the objective function for a given model.""" r = self.W * (self.mapping * m) return 0.5 * r.dot(r) def deriv(self, m): - """ - First derivative with respect to the model - - :param numpy.ndarray m: model - """ + # Docstring inherited from BaseObjectiveFunction return self.mapping.deriv(m).T * (self.W.T * (self.W * (self.mapping * m))) def deriv2(self, m, v=None): - """ - Second derivative with respect to the model - - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply by - """ + # Docstring inherited from BaseObjectiveFunction if v is not None: return self.mapping.deriv(m).T * ( self.W.T * (self.W * (self.mapping.deriv(m) * v)) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index 16c9e3d7d1..fd2d525f4d 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -290,8 +290,7 @@ def Jtvec_approx(self, m, v, f=None): @count def residual(self, m, dobs, f=None): - r""" - The data residual: + r"""The data residual. .. math:: @@ -301,6 +300,7 @@ def residual(self, m, dobs, f=None): :param numpy.ndarray f: fields :rtype: numpy.ndarray :return: data residual + """ return mkvc(self.dpred(m, f=f) - dobs) From e3cd6ed869d5e49ab817b45f1fec037ac638a68f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 11 Jan 2024 12:23:57 -0800 Subject: [PATCH 320/455] Fix description of source_field in gravity survey (#1322) Edits the docstring of `gravity.Survey` to correctly describe the `source_field` argument. --- SimPEG/potential_fields/gravity/survey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SimPEG/potential_fields/gravity/survey.py b/SimPEG/potential_fields/gravity/survey.py index 25bb07759d..7b3b01cf60 100644 --- a/SimPEG/potential_fields/gravity/survey.py +++ b/SimPEG/potential_fields/gravity/survey.py @@ -8,8 +8,8 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.magnetics.sources.SourceField - A source object that defines the Earth's inducing field + source_field : SimPEG.potential_fields.gravity.sources.SourceField + A source object that defines receivers locations for gravity. """ def __init__(self, source_field, **kwargs): From eef5e70557d2f9407485bef0542863c9d13ef43f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 12 Jan 2024 15:59:35 -0800 Subject: [PATCH 321/455] Add `weights_keys` method to `BaseRegularization` (#1320) Add a new `weights_keys` method to `BaseRegularization` that exposes the keys of the private `_weights` attribute. Add tests for the new feature. --- SimPEG/regularization/base.py | 7 ++++ tests/base/test_regularization.py | 69 ++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 1c89fb4ec4..8e91c704d1 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -383,6 +383,13 @@ def set_weights(self, **weights): self._weights[key] = values self._W = None + @property + def weights_keys(self) -> list[str]: + """ + Return the keys for the existing cell weights + """ + return list(self._weights.keys()) + def remove_weights(self, key): """Removes the weights for the key provided. diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index ceb910baf2..8e9f769b91 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -6,7 +6,13 @@ import discretize from SimPEG import maps, objective_function, regularization, utils -from SimPEG.regularization import BaseRegularization, WeightedLeastSquares +from SimPEG.regularization import ( + BaseRegularization, + WeightedLeastSquares, + Smallness, + SmoothnessFirstOrder, + SmoothnessSecondOrder, +) from SimPEG.objective_function import ComboObjectiveFunction @@ -657,6 +663,67 @@ class Dummy: regularization.parent = invalid_parent +class TestWeightsKeys: + """ + Test weights_keys property of regularizations + """ + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([8, 7, 6]) + + def test_empty_weights(self, mesh): + """ + Test weights_keys when no weight is defined + """ + reg = BaseRegularization(mesh) + assert reg.weights_keys == [] + + def test_user_defined_weights_as_dict(self, mesh): + """ + Test weights_keys after user defined weights as dictionary + """ + weights = dict(dummy_weight=np.ones(mesh.n_cells)) + reg = BaseRegularization(mesh, weights=weights) + assert reg.weights_keys == ["dummy_weight"] + + def test_user_defined_weights_as_array(self, mesh): + """ + Test weights_keys after user defined weights as dictionary + """ + weights = np.ones(mesh.n_cells) + reg = BaseRegularization(mesh, weights=weights) + assert reg.weights_keys == ["user_weights"] + + @pytest.mark.parametrize( + "regularization_class", (Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder) + ) + def test_volume_weights(self, mesh, regularization_class): + """ + Test weights_keys has "volume" by default on some regularizations + """ + reg = regularization_class(mesh) + assert reg.weights_keys == ["volume"] + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder), + ) + def test_multiple_weights(self, mesh, regularization_class): + """ + Test weights_keys has "volume" by default on some regularizations + """ + weights = dict( + dummy_weight=np.ones(mesh.n_cells), other_weights=np.ones(mesh.n_cells) + ) + reg = regularization_class(mesh, weights=weights) + if regularization_class == BaseRegularization: + assert reg.weights_keys == ["dummy_weight", "other_weights"] + else: + assert reg.weights_keys == ["dummy_weight", "other_weights", "volume"] + + class TestDeprecatedArguments: """ Test errors after simultaneously passing new and deprecated arguments. From df0579c3c788e4a8201531f9bd4db6c017ef6817 Mon Sep 17 00:00:00 2001 From: johnw Date: Sun, 14 Jan 2024 12:44:09 -0800 Subject: [PATCH 322/455] Update test_regularization.py to test vector derivatives. Update AmplitudeSmoothnessFirstOrder in vector.py so that the call matches the derivative. --- SimPEG/regularization/vector.py | 6 ++++-- tests/base/test_regularization.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 0dd37aad13..0768663105 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -917,9 +917,11 @@ def f_m(self, m): numpy.ndarray The regularization kernel function evaluated for the model provided. """ - a = self.amplitude(m) + fm = (sp.block_diag([self.cell_gradient] * self.n_comp) * (self.mapping * self._delta_m(m))).reshape( + (-1, self.n_comp), order="F" + ) - return self.cell_gradient @ a + return np.linalg.norm(fm, axis=1) def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 8e9f769b91..1f785fe99b 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -580,6 +580,8 @@ def test_vector_amplitude(self): reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) ) + reg.test(model.flatten(order="F")) + def test_WeightedLeastSquares(): mesh = discretize.TensorMesh([3, 4, 5]) From 87bd11ace569a9475283bd2b3296013d2207581e Mon Sep 17 00:00:00 2001 From: johnw Date: Sun, 14 Jan 2024 13:27:26 -0800 Subject: [PATCH 323/455] Update test_regularization.py to test vector derivatives. Update AmplitudeSmoothnessFirstOrder in vector.py so that the call matches the derivative. --- SimPEG/regularization/vector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 0768663105..3d1f8fde8e 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -917,9 +917,10 @@ def f_m(self, m): numpy.ndarray The regularization kernel function evaluated for the model provided. """ - fm = (sp.block_diag([self.cell_gradient] * self.n_comp) * (self.mapping * self._delta_m(m))).reshape( - (-1, self.n_comp), order="F" - ) + fm = ( + sp.block_diag([self.cell_gradient] * self.n_comp) + * (self.mapping * self._delta_m(m)) + ).reshape((-1, self.n_comp), order="F") return np.linalg.norm(fm, axis=1) From 5cb5d634418c5f7047cb1f5fc55b6fc3815d7e97 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 16 Jan 2024 13:48:57 -0800 Subject: [PATCH 324/455] Bump versions of flake8 and black and pin flake plugins (#1330) Bump versions of `black`, `flake8` and flake plugins to their latest available versions in PyPI and in conda-forge. Pin versions of `flake8` and its plugins in the `requirements_style.txt`, `environment_test.yml` and `pre-commit` configuration files. This prevents inconsistencies between the Style Checks run in Azure and the ones run locally through `pre-commit` or `make flake`. --- .pre-commit-config.yaml | 11 ++++++++--- environment_test.yml | 14 +++++++------- requirements_dev.txt | 2 +- requirements_style.txt | 14 +++++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9f4b30ca2..9697f155ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,17 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.12.1 hooks: - id: black language_version: python3 - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-bugbear, flake8-builtins, flake8-mutable, flake8-rst-docstrings, flake8-docstrings] + additional_dependencies: + - flake8-bugbear==23.12.2 + - flake8-builtins==2.2.0 + - flake8-mutable==1.2.0 + - flake8-rst-docstrings==0.3.0 + - flake8-docstrings==1.7.0 diff --git a/environment_test.yml b/environment_test.yml index 888c2180a0..8c5604edb7 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -40,10 +40,10 @@ dependencies: - choclo # Linters and code style - pre-commit - - black==23.1.0 - - flake8 - - flake8-bugbear - - flake8-builtins - - flake8-mutable - - flake8-rst-docstrings - - flake8-docstrings + - black==23.12.1 + - flake8==7.0.0 + - flake8-bugbear==23.12.2 + - flake8-builtins==2.2.0 + - flake8-mutable==1.2.0 + - flake8-rst-docstrings==0.3.0 + - flake8-docstrings==1.7.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index fdda338eac..f81dbe7d4e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -19,7 +19,7 @@ jupyter toolz empymod>=2.0.0 scooby -black==23.1.0 +black==23.12.1 pre-commit twine memory_profiler diff --git a/requirements_style.txt b/requirements_style.txt index 2d054d9fb1..86051e527b 100644 --- a/requirements_style.txt +++ b/requirements_style.txt @@ -1,7 +1,7 @@ -black==23.1.0 -flake8 -flake8-bugbear -flake8-builtins -flake8-mutable -flake8-rst-docstrings -flake8-docstrings +black==23.12.1 +flake8==7.0.0 +flake8-bugbear==23.12.2 +flake8-builtins==2.2.0 +flake8-mutable==1.2.0 +flake8-rst-docstrings==0.3.0 +flake8-docstrings==1.7.0 From 9ec8ee508ae84cce319a6f945e2c66b9eb9f03c3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 16 Jan 2024 15:54:43 -0800 Subject: [PATCH 325/455] Move `__init__` in `BaseSimulation` to the top of the class (#1323) Move the `__init__`` method in `BaseSimulation` to the top of the class. This just improves readability and shouldn't change the behaviour of the class. --- SimPEG/simulation.py | 57 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index fd2d525f4d..c35bd484dd 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -46,6 +46,33 @@ class BaseSimulation(props.HasModel): SimPEG. """ + def __init__( + self, + mesh=None, + survey=None, + solver=None, + solver_opts=None, + sensitivity_path=None, + counter=None, + verbose=False, + **kwargs, + ): + self.mesh = mesh + self.survey = survey + if solver is None: + solver = DefaultSolver + self.solver = solver + if solver_opts is None: + solver_opts = {} + self.solver_opts = solver_opts + if sensitivity_path is None: + sensitivity_path = os.path.join(".", "sensitivity") + self.sensitivity_path = sensitivity_path + self.counter = counter + self.verbose = verbose + + super().__init__(**kwargs) + ########################################################################### # Properties @@ -163,36 +190,6 @@ def verbose(self): def verbose(self, value): self._verbose = validate_type("verbose", value, bool) - ########################################################################### - # Instantiation - - def __init__( - self, - mesh=None, - survey=None, - solver=None, - solver_opts=None, - sensitivity_path=None, - counter=None, - verbose=False, - **kwargs, - ): - self.mesh = mesh - self.survey = survey - if solver is None: - solver = DefaultSolver - self.solver = solver - if solver_opts is None: - solver_opts = {} - self.solver_opts = solver_opts - if sensitivity_path is None: - sensitivity_path = os.path.join(".", "sensitivity") - self.sensitivity_path = sensitivity_path - self.counter = counter - self.verbose = verbose - - super().__init__(**kwargs) - ########################################################################### # Methods From 9fbd787f8bd28d6e81f41b56114cd7dbc951e164 Mon Sep 17 00:00:00 2001 From: johnw Date: Wed, 17 Jan 2024 05:50:00 -0800 Subject: [PATCH 326/455] Update test_regularization.py to test vector derivatives. Update AmplitudeSmoothnessFirstOrder in vector.py so that the call matches the derivative. --- SimPEG/regularization/vector.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 3d1f8fde8e..2341b4d82c 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -917,10 +917,9 @@ def f_m(self, m): numpy.ndarray The regularization kernel function evaluated for the model provided. """ - fm = ( - sp.block_diag([self.cell_gradient] * self.n_comp) - * (self.mapping * self._delta_m(m)) - ).reshape((-1, self.n_comp), order="F") + fm = self.cell_gradient * (self.mapping * self._delta_m(m)).reshape( + (self.regularization_mesh.nC, self.n_comp), order="F" + ) return np.linalg.norm(fm, axis=1) From 926fb1c7b57edd806016df31c88277358032c8fa Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 18 Jan 2024 16:00:07 -0800 Subject: [PATCH 327/455] Fix typo in error messages (#1324) Fix typo on "simultaneously" in various error messages. --- SimPEG/regularization/base.py | 6 +++--- tests/base/test_regularization.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 8e91c704d1..5d092c121a 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -71,7 +71,7 @@ def __init__( if (key := "indActive") in kwargs: if active_cells is not None: raise ValueError( - f"Cannot simultanously pass 'active_cells' and '{key}'. " + f"Cannot simultaneously pass 'active_cells' and '{key}'. " "Pass 'active_cells' only." ) warnings.warn( @@ -86,7 +86,7 @@ def __init__( if (key := "cell_weights") in kwargs: if weights is not None: raise ValueError( - f"Cannot simultanously pass 'weights' and '{key}'. " + f"Cannot simultaneously pass 'weights' and '{key}'. " "Pass 'weights' only." ) warnings.warn( @@ -1588,7 +1588,7 @@ def __init__( if (key := "indActive") in kwargs: if active_cells is not None: raise ValueError( - f"Cannot simultanously pass 'active_cells' and '{key}'. " + f"Cannot simultaneously pass 'active_cells' and '{key}'. " "Pass 'active_cells' only." ) warnings.warn( diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 1f785fe99b..43bcc168ad 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -757,7 +757,7 @@ def mesh(self, request): def test_active_cells(self, mesh, regularization_class): """Test indActive and active_cells arguments.""" active_cells = np.ones(len(mesh), dtype=bool) - msg = "Cannot simultanously pass 'active_cells' and 'indActive'." + msg = "Cannot simultaneously pass 'active_cells' and 'indActive'." with pytest.raises(ValueError, match=msg): regularization_class( mesh, active_cells=active_cells, indActive=active_cells @@ -766,7 +766,7 @@ def test_active_cells(self, mesh, regularization_class): def test_weights(self, mesh): """Test cell_weights and weights.""" weights = np.ones(len(mesh)) - msg = "Cannot simultanously pass 'weights' and 'cell_weights'." + msg = "Cannot simultaneously pass 'weights' and 'cell_weights'." with pytest.raises(ValueError, match=msg): BaseRegularization(mesh, weights=weights, cell_weights=weights) From 0b507fbdd7f49c20700fdcd1ce721b61d21c5b6d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 19 Jan 2024 10:04:42 -0800 Subject: [PATCH 328/455] Extend test to the other orientations Rename the test function to be more descriptive and add a docstring. --- tests/base/test_regularization.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index f66f2bd1e2..82abaca799 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -638,11 +638,17 @@ def test_cross_reg_reg_errors(): regularization.CrossReferenceRegularization(mesh, ref_dir) -def test_coterminal_angle(): +@pytest.mark.parametrize("orientation", ("x", "y", "z")) +def test_smoothness_first_order_coterminal_angle(orientation): + """ + Test smoothness first order regularizations of angles on a treemesh + """ mesh = discretize.TreeMesh([16, 16, 16]) mesh.insert_cells([100, 100, 100], mesh.max_level, finalize=True) - reg = regularization.SmoothnessFirstOrder(mesh, units="radian", orientation="y") + reg = regularization.SmoothnessFirstOrder( + mesh, units="radian", orientation=orientation + ) angles = np.ones(mesh.n_cells) * np.pi angles[5] = -np.pi assert np.all(reg.f_m(angles) == 0) From 392e227e45d78c84801ee48ce90c9f2e725430a7 Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 19 Jan 2024 14:47:01 -0800 Subject: [PATCH 329/455] Remove negative sign --- SimPEG/utils/mat_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index cbf9221d3f..3a01ffa80d 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -411,7 +411,7 @@ def coterminal(theta): """ sub = theta[np.abs(theta) >= np.pi] - sub = -np.sign(sub) * (2 * np.pi - np.abs(sub)) + sub = np.sign(sub) * (2 * np.pi - np.abs(sub)) theta[np.abs(theta) >= np.pi] = sub From 33bba595fd7ed646176d7fed51c63c55f018028e Mon Sep 17 00:00:00 2001 From: domfournier Date: Sat, 20 Jan 2024 11:18:02 -0800 Subject: [PATCH 330/455] CHange to coterminal --- SimPEG/utils/mat_utils.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index 3a01ffa80d..68560176a5 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -409,13 +409,8 @@ def coterminal(theta): Coterminal angles """ - - sub = theta[np.abs(theta) >= np.pi] - sub = np.sign(sub) * (2 * np.pi - np.abs(sub)) - - theta[np.abs(theta) >= np.pi] = sub - - return theta + coterminal = -((theta + np.pi) % (2 * np.pi) - np.pi) + return coterminal def define_plane_from_points(xyz1, xyz2, xyz3): From a7fa543f6d95b89204270e89fe2e7f418a48c269 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sat, 20 Jan 2024 12:01:56 -0800 Subject: [PATCH 331/455] Change coterminal expression again --- SimPEG/utils/mat_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index 68560176a5..8d527f026c 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -409,7 +409,7 @@ def coterminal(theta): Coterminal angles """ - coterminal = -((theta + np.pi) % (2 * np.pi) - np.pi) + coterminal = np.sign(theta) * (np.abs(theta) % np.pi) return coterminal From b836a27208dbca5abc673e9199f4eedcbf68355f Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 21 Jan 2024 11:11:44 -0800 Subject: [PATCH 332/455] Switch to gradient.sign() --- SimPEG/regularization/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 5d092c121a..e1b2956cd3 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -1007,14 +1007,14 @@ def f_m(self, m): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 """ - dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) + dfm_dl = self.mapping * self._delta_m(m) if self.units is not None and self.units.lower() == "radian": return ( - utils.mat_utils.coterminal(dfm_dl * self._cell_distances) + utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) / self._cell_distances ) - return dfm_dl + return self.cell_gradient @ dfm_dl def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. From 3e05248b53be9047ec772dc83cdad6c8f4262890 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 21 Jan 2024 11:18:31 -0800 Subject: [PATCH 333/455] Apply sign to second SmoothnessSecondOrder --- SimPEG/regularization/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index e1b2956cd3..656c1a2572 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -1274,17 +1274,17 @@ def f_m(self, m): .. math:: \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 """ - dfm_dl = self.cell_gradient @ (self.mapping * self._delta_m(m)) + dfm_dl = self.mapping * self._delta_m(m) if self.units is not None and self.units.lower() == "radian": - dfm_dl = ( - utils.mat_utils.coterminal(dfm_dl * self.length_scales) + return self.cell_gradient.T @ ( + utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) / self.length_scales ) - dfm_dl2 = self.cell_gradient.T @ dfm_dl + dfm_dl2 = self.cell_gradient @ dfm_dl - return dfm_dl2 + return self.cell_gradient.T @ dfm_dl2 def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. From 95c80fa8357c6b4245c8ae55bc5f33a32126bdf8 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 21 Jan 2024 11:44:04 -0800 Subject: [PATCH 334/455] Revert changes to coterminal, to be addressed on ISSUE-1333 --- SimPEG/utils/mat_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index 8d527f026c..5d128d9970 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -409,8 +409,12 @@ def coterminal(theta): Coterminal angles """ - coterminal = np.sign(theta) * (np.abs(theta) % np.pi) - return coterminal + sub = theta[np.abs(theta) >= np.pi] + sub = -np.sign(sub) * (2 * np.pi - np.abs(sub)) + + theta[np.abs(theta) >= np.pi] = sub + + return theta def define_plane_from_points(xyz1, xyz2, xyz3): From 106fc8c4fde2e1134c2e6d0bd92726c484f8e0c8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 22 Jan 2024 16:41:58 -0800 Subject: [PATCH 335/455] Simplify check for invalid multipliers Simplify the check for invalid multipliers in `ComboObjectiveFunction`. Move private methods used for validation of arguments to private functions since they didn't need any other attribute of the class. Add tests for the new checks for invalid multipliers. --- SimPEG/objective_function.py | 118 +++++++++++++------------- tests/base/test_objective_function.py | 37 ++++++++ 2 files changed, 98 insertions(+), 57 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b25ccb4c4a..3e2b347f30 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -1,3 +1,4 @@ +import numbers import numpy as np import scipy.sparse as sp @@ -9,6 +10,8 @@ __all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] +VALID_MULTIPLIERS = (numbers.Number, Zero) + class BaseObjectiveFunction(BaseSimPEG): """Base class for creating objective functions. @@ -358,8 +361,6 @@ class ComboObjectiveFunction(BaseObjectiveFunction): """ - _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): # Define default lists if None if objfcts is None: @@ -368,9 +369,10 @@ def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): multipliers = len(objfcts) * [1] # Validate inputs - self._check_length_objective_funcs_multipliers(objfcts, multipliers) - self._validate_objective_functions(objfcts) - self._validate_multipliers(multipliers) + _check_length_objective_funcs_multipliers(objfcts, multipliers) + _validate_objective_functions(objfcts) + for multiplier in multipliers: + _validate_multiplier(multiplier) # Get number of parameters (nP) from objective functions number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] @@ -413,8 +415,9 @@ def multipliers(self): @multipliers.setter def multipliers(self, value): """Set multipliers attribute after checking if they are valid.""" - self._validate_multipliers(value) - self._check_length_objective_funcs_multipliers(self.objfcts, value) + for multiplier in value: + _validate_multiplier(multiplier) + _check_length_objective_funcs_multipliers(self.objfcts, value) self._multipliers = value def __call__(self, m, f=None): @@ -518,56 +521,6 @@ def get_functions_of_type(self, fun_class) -> list: return [fun for fun in target if fun] - def _validate_objective_functions(self, objective_functions): - """Validate objective functions. - - Check if the objective functions have the right types, and if - they all have the same number of parameters. - - """ - for function in objective_functions: - if not isinstance(function, BaseObjectiveFunction): - raise TypeError( - "Unrecognized objective function type " - f"{function.__class__.__name__} in 'objfcts'. " - "All objective functions must inherit from BaseObjectiveFunction." - ) - number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] - if number_of_parameters: - all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) - if not all_equal: - np_list = [f.nP for f in objective_functions] - raise ValueError( - f"Invalid number of parameters '{np_list}' found in " - "objective functions. Except for the ones with '*', they all " - "must have the same number of parameters." - ) - - def _validate_multipliers(self, multipliers): - """Validate multipliers. - - Check if the multipliers have the right types. - - """ - for multiplier in multipliers: - if type(multiplier) not in self._multiplier_types: - valid_types = ", ".join(str(t) for t in self._multiplier_types) - raise TypeError( - f"Invalid multiplier '{multiplier}' of type '{type(multiplier)}'. " - "Objective functions can only be multiplied by " + valid_types - ) - - def _check_length_objective_funcs_multipliers( - self, objective_functions, multipliers - ): - """Check if objective functions and multipliers have the same length.""" - if len(objective_functions) != len(multipliers): - raise ValueError( - "Inconsistent number of elements between objective functions " - f"('{len(objective_functions)}') and multipliers " - f"('{len(multipliers)}'). They must have the same number of parameters." - ) - class L2ObjectiveFunction(BaseObjectiveFunction): r"""Weighted least-squares objective function class. @@ -658,3 +611,54 @@ def deriv2(self, m, v=None): ) W = self.W * self.mapping.deriv(m) return W.T * W + + +def _validate_objective_functions(objective_functions): + """ + Validate objective functions. + + Check if the objective functions have the right types, and if + they all have the same number of parameters. + """ + for function in objective_functions: + if not isinstance(function, BaseObjectiveFunction): + raise TypeError( + "Unrecognized objective function type " + f"{function.__class__.__name__} in 'objfcts'. " + "All objective functions must inherit from BaseObjectiveFunction." + ) + number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] + if number_of_parameters: + all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) + if not all_equal: + np_list = [f.nP for f in objective_functions] + raise ValueError( + f"Invalid number of parameters '{np_list}' found in " + "objective functions. Except for the ones with '*', they all " + "must have the same number of parameters." + ) + + +def _validate_multiplier(multiplier): + """ + Validate multiplier. + + Check if the multiplier is of a valid type. + """ + if not isinstance(multiplier, VALID_MULTIPLIERS) or isinstance(multiplier, bool): + raise TypeError( + f"Invalid multiplier '{multiplier}' of type '{type(multiplier)}'. " + "Objective functions can only be multiplied by scalar numbers." + ) + + +def _check_length_objective_funcs_multipliers(objective_functions, multipliers): + """ + Check if objective functions and multipliers have the same length. + """ + if len(objective_functions) != len(multipliers): + raise ValueError( + "Inconsistent number of elements between objective functions " + f"('{len(objective_functions)}') and multipliers " + f"('{len(multipliers)}'). They must have the same number of parameters." + ) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 1280245b8d..78d2161361 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -5,6 +5,8 @@ from SimPEG import utils, maps from SimPEG import objective_function +from SimPEG.objective_function import _validate_multiplier +from SimPEG.utils import Zero np.random.seed(130) @@ -444,5 +446,40 @@ class Dummy: objective_function.ComboObjectiveFunction(objfcts=[phi, invalid_phi]) +class TestMultiplierValidation: + """ + Test the _validate_multiplier private function. + """ + + @pytest.mark.parametrize( + "multiplier", + ( + 3.14, + 1, + np.float64(-15.3), + np.float32(-10.2), + np.int64(10), + np.int32(33), + Zero(), + ), + ) + def test_valid_multipliers(self, multiplier): + """ + Test function against valid multipliers + """ + _validate_multiplier(multiplier) + + @pytest.mark.parametrize( + "multiplier", + (np.array([1, 3.14]), np.array(3), [1, 2, 3], "string", True, None), + ) + def test_invalid_multipliers(self, multiplier): + """ + Test function against invalid multipliers + """ + with pytest.raises(TypeError, match="Invalid multiplier"): + _validate_multiplier(multiplier) + + if __name__ == "__main__": unittest.main() From cb365eccce0bfc02f37064e714b51bb3af76b044 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Tue, 23 Jan 2024 22:15:23 -0800 Subject: [PATCH 336/455] define the kernels of the linear function on nodes rather than at cell centers --- SimPEG/simulation.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index fd2d525f4d..fd84ef831b 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -567,6 +567,14 @@ class ExponentialSinusoidSimulation(LinearSimulation): \int_x e^{p j_k x} \cos(\pi q j_k x) \quad, j_k \in [j_0, ..., j_n] """ + def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): + self.n_kernels = n_kernels + self.p = p + self.q = q + self.j0 = j0 + self.jn = jn + super(ExponentialSinusoidSimulation, self).__init__(**kwargs) + @property def n_kernels(self): """The number of kernels for the linear problem @@ -637,14 +645,6 @@ def jn(self): def jn(self, value): self._jn = validate_float("jn", value) - def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): - self.n_kernels = n_kernels - self.p = p - self.q = q - self.j0 = j0 - self.jn = jn - super(ExponentialSinusoidSimulation, self).__init__(**kwargs) - @property def jk(self): """ @@ -658,8 +658,8 @@ def g(self, k): """ Kernel functions for the decaying oscillating exponential functions. """ - return np.exp(self.p * self.jk[k] * self.mesh.cell_centers_x) * np.cos( - np.pi * self.q * self.jk[k] * self.mesh.cell_centers_x + return np.exp(self.p * self.jk[k] * self.mesh.nodes_x) * np.cos( + np.pi * self.q * self.jk[k] * self.mesh.nodes_x ) @property @@ -671,7 +671,9 @@ def G(self): G = np.empty((self.n_kernels, self.mesh.nC)) for i in range(self.n_kernels): - G[i, :] = self.g(i) * self.mesh.h[0] + G[i, :] = self.mesh.cell_volumes * ( + self.mesh.average_node_to_cell @ self.g(i) + ) self._G = G return self._G From 5782d4eebed4844bcbb850b3fcea272f63791558 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sat, 27 Jan 2024 21:52:59 -0800 Subject: [PATCH 337/455] remove test checking entries of the kernels --- tests/base/test_simulation.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/base/test_simulation.py b/tests/base/test_simulation.py index a1271d96b2..03a59c7fd7 100644 --- a/tests/base/test_simulation.py +++ b/tests/base/test_simulation.py @@ -19,32 +19,6 @@ def setUp(self): self.mtrue = mtrue - def test_forward(self): - data = np.r_[ - 7.50000000e-02, - 5.34102961e-02, - 5.26315566e-03, - -3.92235199e-02, - -4.22361894e-02, - -1.29419602e-02, - 1.30060891e-02, - 1.73572943e-02, - 7.78056876e-03, - -1.49689823e-03, - -4.50212858e-03, - -3.14559131e-03, - -9.55761370e-04, - 3.53963158e-04, - 7.24902205e-04, - 6.06022770e-04, - 3.36635644e-04, - 7.48637479e-05, - -1.10094573e-04, - -1.84905476e-04, - ] - - assert np.allclose(data, self.sim.dpred(self.mtrue)) - def test_make_synthetic_data(self): dclean = self.sim.dpred(self.mtrue) data = self.sim.make_synthetic_data(self.mtrue) From ebd4b7ab9d1893f8d2d4458a54eace2cc224afd3 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sat, 27 Jan 2024 22:21:26 -0800 Subject: [PATCH 338/455] Update SimPEG/regularization/regularization_mesh.py Co-authored-by: Joseph Capriotti --- SimPEG/regularization/regularization_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 04cdb0c66c..63f185d487 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -551,7 +551,7 @@ def cell_distances_x(self) -> np.ndarray: Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: - self._cell_distances_x = self.cell_gradient_x.max(axis=1).data ** (-1.0) + self._cell_distances_x = self.cell_gradient_x.max(axis=1).toarray().flatten() ** (-1.0) return self._cell_distances_x From b66482a386649ef913828aeec9c3701d96e148d8 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 28 Jan 2024 07:30:24 -0800 Subject: [PATCH 339/455] Apply joe's suggestion to other components. --- SimPEG/regularization/regularization_mesh.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 63f185d487..c07f92504a 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -551,7 +551,9 @@ def cell_distances_x(self) -> np.ndarray: Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: - self._cell_distances_x = self.cell_gradient_x.max(axis=1).toarray().flatten() ** (-1.0) + self._cell_distances_x = self.cell_gradient_x.max( + axis=1 + ).toarray().flatten() ** (-1.0) return self._cell_distances_x @@ -565,7 +567,9 @@ def cell_distances_y(self) -> np.ndarray: Cell center distance array along the y-direction. """ if getattr(self, "_cell_distances_y", None) is None: - self._cell_distances_y = self.cell_gradient_y.max(axis=1).data ** (-1.0) + self._cell_distances_y = self.cell_gradient_y.max( + axis=1 + ).toarray().flatten() ** (-1.0) return self._cell_distances_y @@ -579,7 +583,9 @@ def cell_distances_z(self) -> np.ndarray: Cell center distance array along the z-direction. """ if getattr(self, "_cell_distances_z", None) is None: - self._cell_distances_z = self.cell_gradient_z.max(axis=1).data ** (-1.0) + self._cell_distances_z = self.cell_gradient_z.max( + axis=1 + ).toarray().flatten() ** (-1.0) return self._cell_distances_z From 8bfb0ddca087de46b7f24a6da3e01aff493e3b5c Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 28 Jan 2024 07:56:24 -0800 Subject: [PATCH 340/455] Set function's parent on init. Augment unitests --- SimPEG/objective_function.py | 11 ++++++++++- tests/base/test_regularization.py | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index b25ccb4c4a..3b6e08a278 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -360,7 +360,12 @@ class ComboObjectiveFunction(BaseObjectiveFunction): _multiplier_types = (float, None, Zero, np.float64, int, np.integer) - def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): + def __init__( + self, + objfcts: list[BaseObjectiveFunction] | None = None, + multipliers=None, + unpack_on_add=True, + ): # Define default lists if None if objfcts is None: objfcts = [] @@ -380,6 +385,10 @@ def __init__(self, objfcts=None, multipliers=None, unpack_on_add=True): nP = None super().__init__(nP=nP) + + for fct in objfcts: + fct.parent = self + self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 43bcc168ad..8446b9fbdc 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -664,6 +664,11 @@ class Dummy: with pytest.raises(TypeError, match=msg): regularization.parent = invalid_parent + def test_default_parent(self, regularization): + """Test setting a default parent class to a BaseRegularization.""" + parent = ComboObjectiveFunction(objfcts=[regularization]) + assert regularization.parent is parent + class TestWeightsKeys: """ From 2bd131b75e8e300585f6fc0d63c6abb5a86eb84b Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 28 Jan 2024 08:27:50 -0800 Subject: [PATCH 341/455] Use future import --- SimPEG/objective_function.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 3b6e08a278..09691d6756 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import scipy.sparse as sp From 0e8a7e6af1537dab19b2d6630a1119532fe89c10 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 29 Jan 2024 11:28:17 -0800 Subject: [PATCH 342/455] ravel --- SimPEG/regularization/regularization_mesh.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index c07f92504a..0300adfc44 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -1,9 +1,9 @@ import numpy as np import scipy.sparse as sp + from SimPEG.utils.code_utils import deprecate_property, validate_active_indices -from .. import props -from .. import utils +from .. import props, utils ############################################################################### # # @@ -553,7 +553,7 @@ def cell_distances_x(self) -> np.ndarray: if getattr(self, "_cell_distances_x", None) is None: self._cell_distances_x = self.cell_gradient_x.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) return self._cell_distances_x @@ -569,7 +569,7 @@ def cell_distances_y(self) -> np.ndarray: if getattr(self, "_cell_distances_y", None) is None: self._cell_distances_y = self.cell_gradient_y.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) return self._cell_distances_y @@ -585,7 +585,7 @@ def cell_distances_z(self) -> np.ndarray: if getattr(self, "_cell_distances_z", None) is None: self._cell_distances_z = self.cell_gradient_z.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) return self._cell_distances_z From 9ebab833e674ddb7b83cfb87f1ef250c907b8c83 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 29 Jan 2024 12:09:24 -0800 Subject: [PATCH 343/455] Move assignement in the validation --- SimPEG/objective_function.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 09691d6756..55fb0dfad0 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -388,9 +388,6 @@ def __init__( super().__init__(nP=nP) - for fct in objfcts: - fct.parent = self - self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add @@ -543,6 +540,8 @@ def _validate_objective_functions(self, objective_functions): f"{function.__class__.__name__} in 'objfcts'. " "All objective functions must inherit from BaseObjectiveFunction." ) + function.parent = self + number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] if number_of_parameters: all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) From 05698fbaccc937df5c1054cb54aa8ac34d0e3127 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 29 Jan 2024 12:10:59 -0800 Subject: [PATCH 344/455] Black flake8 fix --- tests/base/test_regularization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 8446b9fbdc..ea21f5859a 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -665,7 +665,7 @@ class Dummy: regularization.parent = invalid_parent def test_default_parent(self, regularization): - """Test setting a default parent class to a BaseRegularization.""" + """Test setting default parent class to a BaseRegularization.""" parent = ComboObjectiveFunction(objfcts=[regularization]) assert regularization.parent is parent From 716e221e83b986479037ae90ba9787f53d9b7334 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 1 Feb 2024 10:15:38 -0800 Subject: [PATCH 345/455] Fix implementation of coterminal function (#1334) Fix the implementation of the `coterminal` function: use the `mod` operator to ensure that the functions returns the correct coterminal angle defined in $[-\pi, \pi)$. Add more tests. --------- Co-authored-by: Santiago Soler Co-authored-by: Joseph Capriotti --- SimPEG/utils/mat_utils.py | 10 +++------- tests/base/test_utils.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index 5d128d9970..c7b3d07fcb 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -396,7 +396,7 @@ def coterminal(theta): \theta = 2\pi N + \gamma and *N* is an integer, the function returns the value of :math:`\gamma`. - The coterminal angle :math:`\gamma` is within the range :math:`[-\pi , \pi]`. + The coterminal angle :math:`\gamma` is within the range :math:`[-\pi , \pi)`. Parameters ---------- @@ -409,12 +409,8 @@ def coterminal(theta): Coterminal angles """ - sub = theta[np.abs(theta) >= np.pi] - sub = -np.sign(sub) * (2 * np.pi - np.abs(sub)) - - theta[np.abs(theta) >= np.pi] = sub - - return theta + coterminal = (theta + np.pi) % (2 * np.pi) - np.pi + return coterminal def define_plane_from_points(xyz1, xyz2, xyz3): diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index 541ed1642b..5952b755fc 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -1,4 +1,5 @@ import unittest +import pytest import numpy as np import scipy.sparse as sp import os @@ -22,6 +23,7 @@ Counter, download, surface2ind_topo, + coterminal, ) import discretize @@ -342,5 +344,35 @@ def test_downloads(self): shutil.rmtree(os.path.expanduser("./test_url")) +class TestCoterminalAngle: + """ + Tests for the coterminal function + """ + + @pytest.mark.parametrize( + "coterminal_angle", + (1 / 4 * np.pi, 3 / 4 * np.pi, -3 / 4 * np.pi, -1 / 4 * np.pi), + ids=("pi/4", "3/4 pi", "-3/4 pi", "-pi/4"), + ) + def test_angles_in_quadrants(self, coterminal_angle): + """ + Test coterminal for angles in each quadrant + """ + angles = np.array([2 * n * np.pi + coterminal_angle for n in range(-3, 4)]) + np.testing.assert_allclose(coterminal(angles), coterminal_angle) + + @pytest.mark.parametrize( + "coterminal_angle", + (0, np.pi / 2, -np.pi, -np.pi / 2), + ids=("0", "pi/2", "-pi", "-pi/2"), + ) + def test_right_angles(self, coterminal_angle): + """ + Test coterminal for right angles + """ + angles = np.array([2 * n * np.pi + coterminal_angle for n in range(-3, 4)]) + np.testing.assert_allclose(coterminal(angles), coterminal_angle) + + if __name__ == "__main__": unittest.main() From bb3a488642f73c5b4270fad3ff269cc94a1dbf14 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 1 Feb 2024 16:27:55 -0800 Subject: [PATCH 346/455] move averaging outside of matrix construction --- SimPEG/simulation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index fd84ef831b..0a0d9d3c4c 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -8,7 +8,7 @@ from discretize.base import BaseMesh from discretize import TensorMesh -from discretize.utils import unpack_widths +from discretize.utils import unpack_widths, sdiag from . import props from .data import SyntheticData, Data @@ -668,12 +668,12 @@ def G(self): Matrix whose rows are the kernel functions """ if getattr(self, "_G", None) is None: - G = np.empty((self.n_kernels, self.mesh.nC)) + G = np.empty((self.mesh.nC, self.n_kernels)) for i in range(self.n_kernels): - G[i, :] = self.mesh.cell_volumes * ( - self.mesh.average_node_to_cell @ self.g(i) - ) + G[:, i] = self.g(i) - self._G = G + self._G = ( + sdiag(self.mesh.cell_volumes) @ (self.mesh.average_node_to_cell @ G).T + ) return self._G From 67b7bd9892e512e66761caa30801587fb070fbca Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 4 Feb 2024 08:11:26 -0800 Subject: [PATCH 347/455] Move parent setting to WeightedLeastSquares --- SimPEG/objective_function.py | 3 --- SimPEG/regularization/base.py | 5 +++++ tests/base/test_regularization.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index c00aec0942..28464bd66e 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -390,9 +390,6 @@ def __init__( super().__init__(nP=nP) - for fun in objfcts: - fun.parent = self - self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 656c1a2572..4db6c4a1c9 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -1665,8 +1665,13 @@ def __init__( objfcts = kwargs.pop("objfcts") super().__init__(objfcts=objfcts, unpack_on_add=False, **kwargs) + + for fun in objfcts: + fun.parent = self + if active_cells is not None: self.active_cells = active_cells + self.mapping = mapping self.reference_model = reference_model self.reference_model_in_smooth = reference_model_in_smooth diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 3023748b86..6e73479785 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -682,7 +682,8 @@ class Dummy: def test_default_parent(self, regularization): """Test setting default parent class to a BaseRegularization.""" - parent = ComboObjectiveFunction(objfcts=[regularization]) + mesh = discretize.TensorMesh([3, 4, 5]) + parent = WeightedLeastSquares(mesh, objfcts=[regularization]) assert regularization.parent is parent From aafb71ec3fa4cb8298c6bbb92075a387f27d71de Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 9 Feb 2024 10:19:49 -0700 Subject: [PATCH 348/455] Update cross gradient hessian approximation This make the hessian approximation in the cross gradient regularization actually SPD. --- SimPEG/regularization/cross_gradient.py | 70 +++++++++---------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index 9c8193a19f..b3b54cb3c5 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -344,64 +344,44 @@ def deriv2(self, model, v=None): g_m1 = G @ m1 g_m2 = G @ m2 - if v is None: - A = ( - G.T - @ ( - sp.diags(Av.T @ (Av @ g_m2**2)) - - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m2) - ) - @ G - ) - - C = ( - G.T - @ ( - sp.diags(Av.T @ (Av @ g_m1**2)) - - sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m1) - ) - @ G - ) + d11_mid = Av.T @ (Av @ g_m2**2) + d12_mid = -(Av.T @ (Av @ (g_m1 * g_m2))) + d22_mid = Av.T @ (Av @ g_m1**2) - B = None - BT = None + if v is None: + D11_mid = sp.diags(d11_mid) + D12_mid = sp.diags(d12_mid) + D22_mid = sp.diags(d22_mid) if not self.approx_hessian: - # d_m1_d_m2 - B = ( - G.T - @ ( - 2 * sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m2) - - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m1) - - sp.diags(Av.T @ Av @ (g_m1 * g_m2)) - ) - @ G + D11_mid = D11_mid - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m2) + D12_mid = ( + D12_mid + + 2 * sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m2) + - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m1) ) - BT = B.T + D22_mid = D22_mid - sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m1) + D11 = G.T @ D11_mid @ G + D12 = G.T @ D12_mid @ G + D22 = G.T @ D22_mid @ G - return sp.bmat([[A, B], [BT, C]], format="csr") + return sp.bmat([[D11, D12], [D12.T, D22]], format="csr") else: v1, v2 = self.wire_map * v Gv1 = G @ v1 Gv2 = G @ v2 - - p1 = G.T @ ( - (Av.T @ (Av @ g_m2**2)) * Gv1 - g_m2 * (Av.T @ (Av @ (g_m2 * Gv1))) - ) - p2 = G.T @ ( - (Av.T @ (Av @ g_m1**2)) * Gv2 - g_m1 * (Av.T @ (Av @ (g_m1 * Gv2))) - ) - + p1 = G.T @ (d11_mid * Gv1 + d12_mid * Gv2) + p2 = G.T @ (d12_mid * Gv1 + d22_mid * Gv2) if not self.approx_hessian: p1 += G.T @ ( - 2 * g_m1 * (Av.T @ (Av @ (g_m2 * Gv2))) - - g_m2 * (Av.T @ (Av @ (g_m1 * Gv2))) - - (Av.T @ (Av @ (g_m1 * g_m2))) * Gv2 + -g_m2 * (Av.T @ (Av @ (g_m2 * Gv1))) # d11*v1 full addition + + 2 * g_m1 * (Av.T @ (Av @ (g_m2 * Gv2))) # d12*v2 full addition + - g_m2 * (Av.T @ (Av @ (g_m1 * Gv2))) # d12*v2 continued ) p2 += G.T @ ( - 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) - - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) - - (Av.T @ (Av @ (g_m2 * g_m1))) * Gv1 + -g_m1 * (Av.T @ (Av @ (g_m1 * Gv2))) # d22*v2 full addition + + 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) # d12.T*v1 full addition + - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued ) return np.r_[p1, p2] From 50a864e3431f07fd876ec2a19bc31a73e9597271 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 20 Feb 2024 11:47:42 -0800 Subject: [PATCH 349/455] Fix partial derivatives in regularization docs (#1362) Fix LaTeX in second order partial derivatives shown in regularization docstrings. Replace $\frac{\partial \phi^2}{\partial m^2}$ for $\frac{\partial^2 \phi}{\partial m^2}$. --- SimPEG/regularization/correspondence.py | 8 ++++---- SimPEG/regularization/cross_gradient.py | 8 ++++---- SimPEG/regularization/jtv.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index 6f31dd3317..55d0db8765 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -185,10 +185,10 @@ def deriv2(self, model, v=None): .. math:: \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = \begin{bmatrix} - \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ - \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} \end{bmatrix} When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index b3b54cb3c5..5204881bcc 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -310,10 +310,10 @@ def deriv2(self, model, v=None): .. math:: \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = \begin{bmatrix} - \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ - \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} \end{bmatrix} When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index aa55780b47..153b8cd511 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -256,11 +256,11 @@ def deriv2(self, model, v=None): .. math:: \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = \begin{bmatrix} - \dfrac{\partial \phi^2}{\partial \mathbf{m_1}^2} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_1} \partial \mathbf{m_2}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} & \cdots \\ - \dfrac{\partial \phi^2}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & - \dfrac{\partial \phi^2}{\partial \mathbf{m_2}^2} & \; \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} & \; \\ \vdots & \; & \ddots \end{bmatrix} From c6d05c30ea7c574db4eedf979ad51df86ce60eaf Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Tue, 20 Feb 2024 15:06:57 -0800 Subject: [PATCH 350/455] Remove factor of half in data misfits and regularizations (#1326) Remove the factor of 1/2 in front of data misfit and regularization terms. Add a factor of 2 to their derivatives. Update target misfit directives to account for the missing 1/2 factor. Update documentation to remove the factor of 1/2. Update tests accordingly (#1332 for PGI). --------- Co-authored-by: Santiago Soler Co-authored-by: Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> --- SimPEG/data_misfit.py | 12 +-- SimPEG/directives/directives.py | 32 ++++---- SimPEG/directives/pgi_directives.py | 2 +- SimPEG/directives/sim_directives.py | 4 +- SimPEG/objective_function.py | 14 ++-- SimPEG/regularization/__init__.py | 12 +-- SimPEG/regularization/base.py | 72 ++++++++--------- SimPEG/regularization/correspondence.py | 10 +-- SimPEG/regularization/cross_gradient.py | 40 ++++++---- SimPEG/regularization/jtv.py | 6 +- SimPEG/regularization/pgi.py | 52 +++++------- SimPEG/regularization/sparse.py | 34 ++++---- SimPEG/regularization/vector.py | 80 +++++++++++-------- .../plot_tomo_joint_with_volume.py | 16 ++-- tests/base/test_cross_gradient.py | 2 +- tests/base/test_objective_function.py | 8 +- tests/base/test_pgi_regularization.py | 14 ++-- tests/em/em1d/test_EM1D_FD_jac_layers.py | 18 +++-- tests/pf/test_pf_quadtree_inversion_linear.py | 8 +- tests/utils/test_mat_utils.py | 6 +- 20 files changed, 226 insertions(+), 216 deletions(-) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py index 42ffc6532d..6d975f2d30 100644 --- a/SimPEG/data_misfit.py +++ b/SimPEG/data_misfit.py @@ -19,7 +19,7 @@ class inherits the :py:class:`SimPEG.objective_function.L2ObjectiveFunction`. create your own data misfit class. .. math:: - \phi_d (\mathbf{m}) = \frac{1}{2} \| \mathbf{W} f(\mathbf{m}) \|_2^2 + \phi_d (\mathbf{m}) = \| \mathbf{W} f(\mathbf{m}) \|_2^2 where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{W}` is a linear weighting matrix, and :math:`f` is a mapping function that acts on the model. @@ -152,7 +152,7 @@ def W(self): For a discrete least-squares data misfit function of the form: .. math:: - \phi_d (\mathbf{m}) = \frac{1}{2} \| \mathbf{W} \mathbf{f}(\mathbf{m}) \|_2^2 + \phi_d (\mathbf{m}) = \| \mathbf{W} \mathbf{f}(\mathbf{m}) \|_2^2 :math:`\mathbf{W}` is a linear weighting matrix, :math:`\mathbf{m}` is the model vector, and :math:`\mathbf{f}` is a discrete mapping function that acts on the model vector. @@ -237,7 +237,7 @@ class L2DataMisfit(BaseDataMisfit): data and predicted data for a given model. I.e.: .. math:: - \phi_d (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W_d} + \phi_d (\mathbf{m}) = \big \| \mathbf{W_d} \big ( \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} \big ) \big \|_2^2 where :math:`\mathbf{d}_\text{obs}` is the observed data vector, :math:`\mathbf{d}_\text{pred}` @@ -266,7 +266,7 @@ def __call__(self, m, f=None): """Evaluate the residual for a given model.""" R = self.W * self.residual(m, f=f) - return 0.5 * np.vdot(R, R) + return np.vdot(R, R) @timeIt def deriv(self, m, f=None): @@ -293,7 +293,7 @@ def deriv(self, m, f=None): if f is None: f = self.simulation.fields(m) - return self.simulation.Jtvec( + return 2 * self.simulation.Jtvec( m, self.W.T * (self.W * self.residual(m, f=f)), f=f ) @@ -330,6 +330,6 @@ def deriv2(self, m, v, f=None): if f is None: f = self.simulation.fields(m) - return self.simulation.Jtvec_approx( + return 2 * self.simulation.Jtvec_approx( m, self.W * (self.W * self.simulation.Jvec_approx(m, v, f=f)), f=f ) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 6700257d9a..6f38db40be 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -1060,17 +1060,17 @@ def phi_d_star(self): ------- float """ - # the factor of 0.5 is because we do phid = 0.5*||dpred - dobs||^2 + # phid = ||dpred - dobs||^2 if self._phi_d_star is None: nD = 0 for survey in self.survey: nD += survey.nD - self._phi_d_star = 0.5 * nD + self._phi_d_star = nD return self._phi_d_star @phi_d_star.setter def phi_d_star(self, value): - # the factor of 0.5 is because we do phid = 0.5*||dpred - dobs||^2 + # phid = ||dpred - dobs||^2 if value is not None: value = validate_float( "phi_d_star", value, min_val=0.0, inclusive_min=False @@ -1166,13 +1166,13 @@ def phi_d_star(self): ------- float """ - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # phid = || dpred - dobs||^2 if getattr(self, "_phi_d_star", None) is None: # Check if it is a ComboObjective if isinstance(self.dmisfit, ComboObjectiveFunction): - value = np.r_[[0.5 * survey.nD for survey in self.survey]] + value = np.r_[[survey.nD for survey in self.survey]] else: - value = np.r_[[0.5 * self.survey.nD]] + value = np.r_[[self.survey.nD]] self._phi_d_star = value self._DMtarget = None @@ -1180,7 +1180,7 @@ def phi_d_star(self): @phi_d_star.setter def phi_d_star(self, value): - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # phid =|| dpred - dobs||^2 if value is not None: value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) self._phi_d_star = value @@ -1426,11 +1426,11 @@ def CLtarget(self): self._CLtarget = self.chiSmall * self.phi_ms_star elif getattr(self, "_CLtarget", None) is None: - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # phid = ||dpred - dobs||^2 if self.phi_ms_star is None: # Expected value is number of active cells * number of physical # properties - self.phi_ms_star = 0.5 * len(self.invProb.model) + self.phi_ms_star = len(self.invProb.model) self._CLtarget = self.chiSmall * self.phi_ms_star @@ -1747,7 +1747,7 @@ def load_results(self): self.f = results[:, 7] - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD self.i_target = None if self.invProb.phi_d < self.target_misfit: @@ -1765,9 +1765,7 @@ def plot_misfit_curves( plot_small=False, plot_smooth=False, ): - self.target_misfit = ( - np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) / 2.0 - ) + self.target_misfit = np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) self.i_target = None if self.invProb.phi_d < self.target_misfit: @@ -1821,7 +1819,7 @@ def plot_misfit_curves( fig.savefig(fname, dpi=dpi) def plot_tikhonov_curves(self, fname=None, dpi=200): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD self.i_target = None if self.invProb.phi_d < self.target_misfit: @@ -2062,7 +2060,7 @@ def target(self): for survey in self.survey: nD += survey.nD - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -2076,10 +2074,10 @@ def start(self): if isinstance(self.survey, list): self._start = 0 for survey in self.survey: - self._start += survey.nD * 0.5 * self.chifact_start + self._start += survey.nD * self.chifact_start else: - self._start = self.survey.nD * 0.5 * self.chifact_start + self._start = self.survey.nD * self.chifact_start return self._start @start.setter diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index db332ff9bb..e8fb543ee1 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -413,7 +413,7 @@ def initialize(self): @property def DMtarget(self): if getattr(self, "_DMtarget", None) is None: - self.phi_d_target = 0.5 * self.invProb.dmisfit.survey.nD + self.phi_d_target = self.invProb.dmisfit.survey.nD self._DMtarget = self.chifact * self.phi_d_target return self._DMtarget diff --git a/SimPEG/directives/sim_directives.py b/SimPEG/directives/sim_directives.py index 5b781fe97a..718fac26c3 100644 --- a/SimPEG/directives/sim_directives.py +++ b/SimPEG/directives/sim_directives.py @@ -305,7 +305,7 @@ def target(self): if getattr(self, "_target", None) is None: nD = np.array([survey.nD for survey in self.survey]) - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -362,7 +362,7 @@ def target(self): nD += [survey.nD] nD = np.array(nD) - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py index 28464bd66e..b3c299cea2 100644 --- a/SimPEG/objective_function.py +++ b/SimPEG/objective_function.py @@ -536,7 +536,7 @@ class L2ObjectiveFunction(BaseObjectiveFunction): Weighting least-squares objective functions in SimPEG are defined as follows: .. math:: - \phi = \frac{1}{2} \big \| \mathbf{W} f(\mathbf{m}) \big \|_2^2 + \phi = \big \| \mathbf{W} f(\mathbf{m}) \big \|_2^2 where :math:`\mathbf{m}` are the model parameters, :math:`f` is a mapping operator, and :math:`\mathbf{W}` is the weighting matrix. @@ -605,20 +605,22 @@ def W(self): def __call__(self, m): """Evaluate the objective function for a given model.""" r = self.W * (self.mapping * m) - return 0.5 * r.dot(r) + return r.dot(r) def deriv(self, m): # Docstring inherited from BaseObjectiveFunction - return self.mapping.deriv(m).T * (self.W.T * (self.W * (self.mapping * m))) + return 2 * self.mapping.deriv(m).T * (self.W.T * (self.W * (self.mapping * m))) def deriv2(self, m, v=None): # Docstring inherited from BaseObjectiveFunction if v is not None: - return self.mapping.deriv(m).T * ( - self.W.T * (self.W * (self.mapping.deriv(m) * v)) + return ( + 2 + * self.mapping.deriv(m).T + * (self.W.T * (self.W * (self.mapping.deriv(m) * v))) ) W = self.W * self.mapping.deriv(m) - return W.T * W + return 2 * W.T * W def _validate_objective_functions(objective_functions): diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 5d1a7910ac..c379bbd202 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -52,10 +52,10 @@ .. math:: \phi_m (m) = - \alpha_s \! \int_\Omega \Bigg [ \frac{1}{2} w_s(r) \, m(r)^2 \Bigg ] \, dv + - \alpha_x \! \int_\Omega \Bigg [ \frac{1}{2} w_x(r) + \alpha_s \! \int_\Omega \Bigg [ w_s(r) \, m(r)^2 \Bigg ] \, dv + + \alpha_x \! \int_\Omega \Bigg [ w_x(r) \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + - \alpha_y \! \int_\Omega \Bigg [ \frac{1}{2} w_y(r) + \alpha_y \! \int_\Omega \Bigg [ w_y(r) \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv where :math:`w_s(r), w_x(r), w_y(r)` are user-defined weighting functions. @@ -65,9 +65,9 @@ And the regularization is implemented using a weighted sum of objective functions: .. math:: - \phi_m (\mathbf{m}) \approx \frac{\alpha_s}{2} \big \| \mathbf{W_s m} \big \|^2 + - \frac{\alpha_x}{2} \big \| \mathbf{W_x G_x m} \big \|^2 + - \frac{\alpha_y}{2} \big \| \mathbf{W_y G_y m} \big \|^2 + \phi_m (\mathbf{m}) \approx \alpha_s \big \| \mathbf{W_s m} \big \|^2 + + \alpha_x \big \| \mathbf{W_x G_x m} \big \|^2 + + \alpha_y \big \| \mathbf{W_y G_y m} \big \|^2 where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradient operators along the x and y-directions, respectively. :math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 4db6c4a1c9..4857a376d2 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -472,7 +472,7 @@ def __call__(self, m): The regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) - return 0.5 * r.dot(r) + return r.dot(r) def f_m(self, m) -> np.ndarray: """Not implemented for ``BaseRegularization`` class.""" @@ -506,7 +506,7 @@ def deriv(self, m) -> np.ndarray: The Gradient of the regularization function evaluated for the model provided. """ r = self.W * self.f_m(m) - return self.f_m_deriv(m).T * (self.W.T * r) + return 2 * self.f_m_deriv(m).T * (self.W.T * r) @utils.timeIt def deriv2(self, m, v=None) -> csr_matrix: @@ -539,9 +539,9 @@ def deriv2(self, m, v=None) -> csr_matrix: """ f_m_deriv = self.f_m_deriv(m) if v is None: - return f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + return 2 * f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) - return f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) + return 2 * f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) class Smallness(BaseRegularization): @@ -584,7 +584,7 @@ class Smallness(BaseRegularization): We define the regularization function (objective function) for smallness as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` @@ -595,7 +595,7 @@ class Smallness(BaseRegularization): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and @@ -604,7 +604,7 @@ class Smallness(BaseRegularization): This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} + \phi (\mathbf{m}) = \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where @@ -674,7 +674,7 @@ def f_m(self, m) -> np.ndarray: The objective function for smallness regularization is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), @@ -689,7 +689,7 @@ def f_m(self, m) -> np.ndarray: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 """ return self.mapping * self._delta_m(m) @@ -720,7 +720,7 @@ def f_m_deriv(self, m) -> csr_matrix: The objective function for smallness regularization is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), @@ -735,7 +735,7 @@ def f_m_deriv(self, m) -> csr_matrix: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 Thus, the derivative with respect to the model is: @@ -794,7 +794,7 @@ class SmoothnessFirstOrder(BaseRegularization): along the x-direction as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. @@ -804,7 +804,7 @@ class SmoothnessFirstOrder(BaseRegularization): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh @@ -813,7 +813,7 @@ class SmoothnessFirstOrder(BaseRegularization): This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, G_x m } \, \Big \|^2 + \phi (\mathbf{m}) = \Big \| \mathbf{W \, G_x m } \, \Big \|^2 where @@ -830,7 +830,7 @@ class SmoothnessFirstOrder(BaseRegularization): In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W G_x} + \phi (\mathbf{m}) = \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting a reference model with the @@ -988,7 +988,7 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), @@ -1005,7 +1005,7 @@ def f_m(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 """ dfm_dl = self.mapping * self._delta_m(m) @@ -1044,7 +1044,7 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), @@ -1061,7 +1061,7 @@ def f_m_deriv(self, m) -> csr_matrix: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 The derivative with respect to the model is therefore: @@ -1159,7 +1159,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): smoothness along the x-direction as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. @@ -1169,7 +1169,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the @@ -1178,7 +1178,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \big \| \mathbf{W \, L_x \, m } \, \big \|^2 + \phi (\mathbf{m}) = \big \| \mathbf{W \, L_x \, m } \, \big \|^2 where @@ -1192,7 +1192,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W L_x} + \phi (\mathbf{m}) = \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting a reference model with the @@ -1255,7 +1255,7 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), @@ -1272,7 +1272,7 @@ def f_m(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 """ dfm_dl = self.mapping * self._delta_m(m) @@ -1313,7 +1313,7 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where :math:`\mathbf{m}` are the discrete model parameters (model), @@ -1330,7 +1330,7 @@ def f_m_deriv(self, m) -> csr_matrix: such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W \, f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 The derivative of the regularization kernel function with respect to the model is: @@ -1433,11 +1433,11 @@ class WeightedLeastSquares(ComboObjectiveFunction): :math:`\phi_m (m)` of the form: .. math:: - \phi_m (m) =& \frac{\alpha_s}{2} \int_\Omega \, w(r) + \phi_m (m) =& \alpha_s \int_\Omega \, w(r) \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + &+ \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^2 \, dv \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \int_\Omega \, w(r) + &+ \sum_{j=x,y,z} \alpha_{jj} \int_\Omega \, w(r) \bigg [ \frac{\partial^2 m}{\partial \xi_j^2} \bigg ]^2 \, dv \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) @@ -1461,10 +1461,10 @@ class WeightedLeastSquares(ComboObjectiveFunction): objective functions of the form: .. math:: - \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) =& \alpha_s \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where @@ -1482,11 +1482,11 @@ class WeightedLeastSquares(ComboObjectiveFunction): In this case, the objective function becomes: .. math:: - \phi_m (\mathbf{m}) =& \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) =& \alpha_s \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j} + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j} + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py index 55d0db8765..670afc3132 100644 --- a/SimPEG/regularization/correspondence.py +++ b/SimPEG/regularization/correspondence.py @@ -43,7 +43,7 @@ class LinearCorrespondence(BaseSimilarityMeasure): .. math:: \phi (\mathbf{m}) - = \frac{1}{2} \big \| \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 \big \|^2 + = \big \| \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 \big \|^2 Scalar coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` are set using the `coefficients` property. For a true linear correspondence constraint, we set @@ -130,7 +130,7 @@ def __call__(self, model): """ result = self.relation(model) - return 0.5 * result.T @ result + return result.T @ result def deriv(self, model): r"""Gradient of the regularization function evaluated for the model provided. @@ -167,7 +167,7 @@ def deriv(self, model): result = np.r_[dc_dm1, dc_dm2] - return result + return 2 * result def deriv2(self, model, v=None): r"""Hessian of the regularization function evaluated for the model provided. @@ -217,10 +217,10 @@ def deriv2(self, model, v=None): v1, v2 = self.wire_map * v p1 = k1**2 * v1 + k2 * k1 * v2 p2 = k2 * k1 * v1 + k2**2 * v2 - return np.r_[p1, p2] + return 2 * np.r_[p1, p2] else: n = self.regularization_mesh.nC A = utils.sdiag(np.ones(n) * (k1**2)) B = utils.sdiag(np.ones(n) * (k2**2)) C = utils.sdiag(np.ones(n) * (k1 * k2)) - return sp.bmat([[A, C], [C, B]], format="csr") + return 2 * sp.bmat([[A, C], [C, B]], format="csr") diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py index 5204881bcc..a1cb7e1c03 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/SimPEG/regularization/cross_gradient.py @@ -52,7 +52,7 @@ class CrossGradient(BaseSimilarityMeasure): (`Haber and Gazit, 2013 `__): .. math:: - \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m_1, m_2) = \int_\Omega \, w(r) \, \Big | \nabla m_1 \, \times \, \nabla m_2 \, \Big |^2 \, dv where :math:`w(r)` is a user-defined weighting function. @@ -60,7 +60,7 @@ class CrossGradient(BaseSimilarityMeasure): the regularization function can be re-expressed as: .. math:: - \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, \Big [ \, + \phi (m_1, m_2) = \int_\Omega \, w(r) \, \Big [ \, \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv @@ -69,7 +69,7 @@ class CrossGradient(BaseSimilarityMeasure): function (objective function) is given by: .. math:: - \phi (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ + \phi (m_1, m_2) \approx \sum_i \tilde{w}_i \, \bigg [ \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] @@ -89,9 +89,9 @@ class CrossGradient(BaseSimilarityMeasure): .. math:: \phi (\mathbf{m}) = - \frac{1}{2} \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T + \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T \Big [ \mathbf{W A} \big ( \mathbf{G \, m_2} \big )^2 \Big ] - - \frac{1}{2} \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) + - \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) \odot \big ( \mathbf{G \, m_2} \big ) \Big ] \bigg \|^2 where exponents are computed elementwise, @@ -249,9 +249,7 @@ def __call__(self, model): G = self._G g_m1 = G @ m1 g_m2 = G @ m2 - return 0.5 * np.sum( - (Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2 - ) + return np.sum((Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2) def deriv(self, model): r"""Gradient of the regularization function evaluated for the model provided. @@ -267,7 +265,7 @@ def deriv(self, model): The gradient has the form: .. math:: - \frac{\partial \phi}{\partial \mathbf{m}} = + 2 \frac{\partial \phi}{\partial \mathbf{m}} = \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} @@ -288,12 +286,15 @@ def deriv(self, model): g_m1 = G @ m1 g_m2 = G @ m2 - return np.r_[ - (((Av @ g_m2**2) @ Av) * g_m1) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m2) @ G, - (((Av @ g_m1**2) @ Av) * g_m2) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m1) @ G, - ] + return ( + 2 + * np.r_[ + (((Av @ g_m2**2) @ Av) * g_m1) @ G + - (((Av @ (g_m1 * g_m2)) @ Av) * g_m2) @ G, + (((Av @ g_m1**2) @ Av) * g_m2) @ G + - (((Av @ (g_m1 * g_m2)) @ Av) * g_m1) @ G, + ] + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 def deriv2(self, model, v=None): r"""Hessian of the regularization function evaluated for the model provided. @@ -364,7 +365,10 @@ def deriv2(self, model, v=None): D12 = G.T @ D12_mid @ G D22 = G.T @ D22_mid @ G - return sp.bmat([[D11, D12], [D12.T, D22]], format="csr") + return 2 * sp.bmat( + [[D11, D12], [D12.T, D22]], format="csr" + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + else: v1, v2 = self.wire_map * v @@ -384,4 +388,6 @@ def deriv2(self, model, v=None): + 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) # d12.T*v1 full addition - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued ) - return np.r_[p1, p2] + return ( + 2 * np.r_[p1, p2] + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py index 153b8cd511..86a2208915 100644 --- a/SimPEG/regularization/jtv.py +++ b/SimPEG/regularization/jtv.py @@ -50,7 +50,7 @@ class JointTotalVariation(BaseSimilarityMeasure): (`Haber and Gazit, 2013 `__): .. math:: - \phi (m_1, m_2) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m_1, m_2) = \int_\Omega \, w(r) \, \Big [ \, \big | \nabla m_1 \big |^2 \, + \, \big | \nabla m_2 \big |^2 \, \Big ]^{1/2} \, dv where :math:`w(r)` is a user-defined weighting function. @@ -60,7 +60,7 @@ class JointTotalVariation(BaseSimilarityMeasure): function (objective function) is given by: .. math:: - \phi (m_1, m_2) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \bigg [ \, + \phi (m_1, m_2) \approx \sum_i \tilde{w}_i \, \bigg [ \, \Big | (\nabla m_1)_i \Big |^2 \, + \, \Big | (\nabla m_2)_i \Big |^2 \, \bigg ]^{1/2} where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and @@ -78,7 +78,7 @@ class JointTotalVariation(BaseSimilarityMeasure): is therefore equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \, \mathbf{e}^T \Bigg ( \, + \phi (\mathbf{m}) = \mathbf{e}^T \Bigg ( \, \mathbf{W \, A} \bigg [ \sum_k (\mathbf{G \, m_k})^2 \bigg ] \; + \; \epsilon \mathbf{v}^2 \, \Bigg )^{1/2} diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 496df06f45..2c98c321f8 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -103,10 +103,10 @@ class PGIsmallness(Smallness): least-square: .. math:: - \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} + \phi (\mathbf{m}) &= \alpha_\text{pgi} \big | \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, (\mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \, \Big \|^2 - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where @@ -497,7 +497,7 @@ def __call__(self, m, external_weights=True): ] ] - return 0.5 * mkvc(r0).dot(mkvc(r1)) + return mkvc(r0).dot(mkvc(r1)) else: modellist = self.wiresmap * m @@ -506,7 +506,7 @@ def __call__(self, m, external_weights=True): if self.non_linear_relationships: score = self.gmm.score_samples(model) score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) - return -np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) + return -2 * np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) else: if external_weights and getattr(self.W, "diagonal", None) is not None: @@ -519,7 +519,7 @@ def __call__(self, m, external_weights=True): score = self.gmm.score_samples_with_sensW(model, sensW) # score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) # return -np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) - return -np.sum(score) + return -2 * np.sum(score) @timeIt def deriv(self, m): @@ -616,7 +616,7 @@ def deriv(self, m): ] ] ) - return mkvc(mD.T * (self.W.T * r)) + return 2 * mkvc(mD.T * (self.W.T * r)) else: if self.non_linear_relationships: @@ -726,7 +726,7 @@ def deriv(self, m): logP = np.vstack([logP for maps in self.wiresmap.maps]) numer = (W * np.exp(logP)).sum(axis=1) r = numer / (np.exp(score_vec)) - return mkvc(mD.T * r) + return 2 * mkvc(mD.T * r) @timeIt def deriv2(self, m, v=None): @@ -841,22 +841,12 @@ def deriv2(self, m, v=None): mDv = self.wiresmap * (mD * v) mDv = np.c_[mDv] r0 = (self.W * (mkvc(mDv))).reshape(mDv.shape, order="F") - return mkvc( - mD.T - * ( - self.W - * ( - mkvc( - np.r_[ - [ - np.dot(self._r_second_deriv[i], r0[i]) - for i in range(len(r0)) - ] - ] - ) - ) - ) + second_deriv_times_r0 = mkvc( + np.r_[ + [np.dot(self._r_second_deriv[i], r0[i]) for i in range(len(r0))] + ] ) + return 2 * mkvc(mD.T * (self.W * second_deriv_times_r0)) else: # Forming the Hessian by diagonal blocks hlist = [ @@ -875,7 +865,7 @@ def deriv2(self, m, v=None): Hr = Hr.dot(self.W) - return (mD.T * mD) * (self.W * (Hr)) + return 2 * (mD.T * mD) * (self.W * (Hr)) else: if self.non_linear_relationships: @@ -953,7 +943,7 @@ def deriv2(self, m, v=None): for j in range(len(self.wiresmap.maps)): Hc = sp.hstack([Hc, sdiag(hlist[i][j])]) Hr = sp.vstack([Hr, Hc]) - Hr = (mD.T * mD) * Hr + Hr = 2 * (mD.T * mD) * Hr if v is not None: return Hr.dot(v) @@ -1041,12 +1031,12 @@ class PGI(ComboObjectiveFunction): ``PGI`` is given by: .. math:: - \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} + \phi (\mathbf{m}) &= \alpha_\text{pgi} \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ]^T \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) where @@ -1072,10 +1062,10 @@ class PGI(ComboObjectiveFunction): regularization function (objective function) can be expressed as: .. math:: - \phi (\mathbf{m}) &= \frac{\alpha_{pgi}}{2} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, + \phi (\mathbf{m}) &= \alpha_\text{pgi} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \frac{\alpha_{jj}}{2} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` property diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index e1602971c0..817c49e224 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -257,7 +257,7 @@ class SparseSmallness(BaseSparse, Smallness): We define the regularization function (objective function) for sparse smallness (compactness) as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` @@ -271,7 +271,7 @@ class SparseSmallness(BaseSparse, Smallness): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_i} where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. @@ -286,8 +286,8 @@ class SparseSmallness(BaseSparse, Smallness): .. math:: \phi \big (\mathbf{m}^{(k)} \big ) - = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} - \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 + = \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -300,7 +300,7 @@ class SparseSmallness(BaseSparse, Smallness): function for IRLS iteration :math:`k` can be expressed as follows: .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{\! (k)} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 where @@ -464,7 +464,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): along the x-direction as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \Bigg | \, \frac{\partial m}{\partial x} \, \Bigg |^{p(r)} \, dv where :math:`m(r)` is the model, :math:`w(r)` @@ -478,7 +478,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^{p_i} where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. @@ -493,9 +493,9 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): .. math:: \phi \big (\mathbf{m}^{(k)} \big ) - = \frac{1}{2} \sum_i + = \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^{p_i} - \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -509,7 +509,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): function for IRLS iteration :math:`k` can be expressed as follows: .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 where @@ -528,7 +528,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): In this case, the least-squares problem for IRLS iteration :math:`k` becomes: .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{(k)} \mathbf{G_x} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 @@ -765,9 +765,9 @@ class Sparse(WeightedLeastSquares): :math:`\phi_m (m)` of the form: .. math:: - \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) + \phi_m (m) = \alpha_s \int_\Omega \, w(r) \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p_s(r)} \, dv - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + + \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) \Bigg | \, \frac{\partial m}{\partial \xi_j} \, \Bigg |^{p_j(r)} \, dv where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` @@ -819,9 +819,9 @@ class Sparse(WeightedLeastSquares): objective functions of the form: .. math:: - \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) = \alpha_s \Big \| \mathbf{W_s}^{\!\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j \, m} \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j \, m} \Big \|^2 where @@ -878,9 +878,9 @@ class Sparse(WeightedLeastSquares): the objective function becomes: .. math:: - \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) = \alpha_s \Big \| \mathbf{W_s}^{\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| + + \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 This functionality is used by setting the `reference_model_in_smooth` parameter diff --git a/SimPEG/regularization/vector.py b/SimPEG/regularization/vector.py index 2341b4d82c..19bd68f080 100644 --- a/SimPEG/regularization/vector.py +++ b/SimPEG/regularization/vector.py @@ -82,7 +82,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): regularization is given by: .. math:: - \phi (\vec{m}) = \frac{1}{2} \int_\Omega \, \vec{w}(r) \, \cdot \, + \phi (\vec{m}) = \int_\Omega \, \vec{w}(r) \, \cdot \, \Big [ \vec{m}(r) \, \times \, \vec{m}^{(ref)}(r) \Big ]^2 \, dv where :math:`\vec{m}^{(ref)}(r)` is the reference model vector and :math:`\vec{w}(r)` @@ -93,7 +93,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): function (objective function) is given by: .. math:: - \phi (\vec{m}) \approx \frac{1}{2} \sum_i \tilde{w}_i \, \cdot \, + \phi (\vec{m}) \approx \sum_i \tilde{w}_i \, \cdot \, \Big | \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big |^2 where :math:`\tilde{m}_i \in \mathbf{m}` are the model vectors at cell centers and @@ -129,7 +129,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): The discrete regularization function in linear form can ultimately be expressed as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} + \phi (\mathbf{m}) = \Big \| \mathbf{W X m} \, \Big \|^2 @@ -262,7 +262,7 @@ def f_m(self, m): The objective function for cross reference regularization is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W X m} \, \Big \|^2 where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), @@ -277,7 +277,7 @@ def f_m(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 """ return self._X @ (self.mapping * m) @@ -309,7 +309,7 @@ def f_m_deriv(self, m): The objective function for cross reference regularization is given by: .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} + \phi_m (\mathbf{m}) = \Big \| \mathbf{W X m} \, \Big \|^2 where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), @@ -324,7 +324,7 @@ def f_m_deriv(self, m): such that .. math:: - \phi_m (\mathbf{m}) = \frac{1}{2} \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 Thus, the derivative with respect to the model is: @@ -423,11 +423,15 @@ def deriv(self, m) -> np.ndarray: """ d_m = self._delta_m(m) - return self.f_m_deriv(m).T * ( - self.W.T - @ self.W - @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order="F") - ).flatten(order="F") + return ( + 2 + * self.f_m_deriv(m).T + * ( + self.W.T + @ self.W + @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") + ) def deriv2(self, m, v=None) -> csr_matrix: r"""Hessian of the regularization function evaluated for the model provided. @@ -460,13 +464,21 @@ def deriv2(self, m, v=None) -> csr_matrix: f_m_deriv = self.f_m_deriv(m) if v is None: - return f_m_deriv.T * ( - sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv + return ( + 2 + * f_m_deriv.T + * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv) ) - return f_m_deriv.T * ( - self.W.T @ self.W @ (f_m_deriv * v).reshape((-1, self.n_comp), order="F") - ).flatten(order="F") + return ( + 2 + * f_m_deriv.T + * ( + self.W.T + @ self.W + @ (f_m_deriv * v).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") + ) class AmplitudeSmallness(SparseSmallness, BaseAmplitude): @@ -519,7 +531,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): (compactness) as: .. math:: - \phi (\vec{m}) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (\vec{m}) = \int_\Omega \, w(r) \, \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p(r)} \, dv where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, :math:`w(r)` @@ -533,7 +545,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \Big | \vec{m}_i - \vec{m}_i^{(ref)} \Big |^{p_i} where :math:`\mathbf{m}` are the model parameters, :math:`\vec{m}_i` represents the vector @@ -549,8 +561,8 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): .. math:: \phi \big (\mathbf{m}^{(k)} \big ) - = \frac{1}{2} \sum_i \tilde{w}_i \, \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^{p_i} - \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + = \sum_i \tilde{w}_i \, \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -578,7 +590,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): The objective function for IRLS iteration :math:`k` is given by: .. math:: - \phi \big ( \mathbf{\bar{m}}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{\bar{m}}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{(k)} \, \mathbf{\bar{m}}^{(k)} \; \Big \|^2 where @@ -738,7 +750,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): along the x-direction as: .. math:: - \phi (m) = \frac{1}{2} \int_\Omega \, w(r) \, + \phi (m) = \int_\Omega \, w(r) \, \Bigg | \, \frac{\partial |\vec{m}|}{\partial x} \, \Bigg |^{p(r)} \, dv where :math:`\vec{m}(r)` is the model, :math:`w(r)` @@ -752,7 +764,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): function (objective function) is expressed in linear form as: .. math:: - \phi (\mathbf{m}) = \frac{1}{2} \sum_i + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, \Bigg | \, \frac{\partial |\vec{m}_i|}{\partial x} \, \Bigg |^{p_i} where :math:`\vec{m}_i` is the vector defined for mesh cell :math:`i`. @@ -767,9 +779,9 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): .. math:: \phi \big (\mathbf{m}^{(k)} \big ) - = \frac{1}{2} \sum_i + = \sum_i \tilde{w}_i \, \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^{p_i} - \approx \frac{1}{2} \sum_i \tilde{w}_i \, r_i^{(k)} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^2 where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: @@ -794,7 +806,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): The objective function for IRLS iteration :math:`k` is given by: .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 where @@ -813,7 +825,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): In this case, the least-squares problem for IRLS iteration :math:`k` becomes: .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \frac{1}{2} \Big \| \, + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 where @@ -1042,9 +1054,9 @@ class VectorAmplitude(Sparse): :math:`\phi_m (m)` of the form: .. math:: - \phi_m (m) = \frac{\alpha_s}{2} \int_\Omega \, w(r) + \phi_m (m) = \alpha_s \int_\Omega \, w(r) \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p_s(r)} \, dv - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \int_\Omega \, w(r) + + \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) \Bigg | \, \frac{\partial |\vec{m}|}{\partial \xi_j} \, \bigg |^{p_j(r)} \, dv where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, @@ -1104,9 +1116,9 @@ class VectorAmplitude(Sparse): objective functions of the form: .. math:: - \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) = \alpha_s \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \bar{m}} \, \Big \|^2 where @@ -1171,9 +1183,9 @@ class VectorAmplitude(Sparse): the objective function becomes: .. math:: - \phi_m (\mathbf{m}) = \frac{\alpha_s}{2} + \phi_m (\mathbf{m}) = \alpha_s \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 - + \sum_{j=x,y,z} \frac{\alpha_j}{2} \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \Delta \bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \Delta \bar{m}} \, \Big \|^2 This functionality is used by setting the `reference_model_in_smooth` parameter to ``True``. diff --git a/examples/20-published/plot_tomo_joint_with_volume.py b/examples/20-published/plot_tomo_joint_with_volume.py index 2b9c445917..791bc32a8c 100644 --- a/examples/20-published/plot_tomo_joint_with_volume.py +++ b/examples/20-published/plot_tomo_joint_with_volume.py @@ -42,7 +42,7 @@ class Volume(objective_function.BaseObjectiveFunction): .. math:: - \phi_v = \frac{1}{2}|| \int_V m dV - \text{knownVolume} ||^2 + \phi_v = || \int_V m dV - \text{knownVolume} ||^2 """ def __init__(self, mesh, knownVolume=0.0, **kwargs): @@ -60,25 +60,27 @@ def knownVolume(self, value): self._knownVolume = utils.validate_float("knownVolume", value, min_val=0.0) def __call__(self, m): - return 0.5 * (self.estVol(m) - self.knownVolume) ** 2 + return (self.estVol(m) - self.knownVolume) ** 2 def estVol(self, m): return np.inner(self.mesh.cell_volumes, m) def deriv(self, m): # return (self.mesh.cell_volumes * np.inner(self.mesh.cell_volumes, m)) - return self.mesh.cell_volumes * ( - self.knownVolume - np.inner(self.mesh.cell_volumes, m) - ) + return ( + 2 + * self.mesh.cell_volumes + * (self.knownVolume - np.inner(self.mesh.cell_volumes, m)) + ) # factor of 2 from deriv of ||estVol - knownVol||^2 def deriv2(self, m, v=None): if v is not None: - return utils.mkvc( + return 2 * utils.mkvc( self.mesh.cell_volumes * np.inner(self.mesh.cell_volumes, v) ) else: # TODO: this is inefficent. It is a fully dense matrix - return sp.csc_matrix( + return 2 * sp.csc_matrix( np.outer(self.mesh.cell_volumes, self.mesh.cell_volumes) ) diff --git a/tests/base/test_cross_gradient.py b/tests/base/test_cross_gradient.py index 66e84082ab..9c764d481e 100644 --- a/tests/base/test_cross_gradient.py +++ b/tests/base/test_cross_gradient.py @@ -96,7 +96,7 @@ def test_cross_grad_calc(self): cross_grad = self.cross_grad - v1 = 0.5 * np.sum(np.abs(cross_grad.calculate_cross_gradient(m))) + v1 = np.sum(np.abs(cross_grad.calculate_cross_gradient(m))) v2 = cross_grad(m) self.assertEqual(v1, v2) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 78d2161361..211a9719bf 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -278,13 +278,11 @@ def test_ComboW(self): r1 = phi1.W * m r2 = phi2.W * m - print(phi(m), 0.5 * np.inner(r, r)) + print(phi(m), np.inner(r, r)) - self.assertTrue(np.allclose(phi(m), 0.5 * np.inner(r, r))) + self.assertTrue(np.allclose(phi(m), np.inner(r, r))) self.assertTrue( - np.allclose( - phi(m), 0.5 * (alpha1 * np.inner(r1, r1) + alpha2 * np.inner(r2, r2)) - ) + np.allclose(phi(m), (alpha1 * np.inner(r1, r1) + alpha2 * np.inner(r2, r2))) ) def test_ComboConstruction(self): diff --git a/tests/base/test_pgi_regularization.py b/tests/base/test_pgi_regularization.py index b8db90f00e..440e50a494 100644 --- a/tests/base/test_pgi_regularization.py +++ b/tests/base/test_pgi_regularization.py @@ -4,6 +4,7 @@ import numpy as np from pymatsolver import SolverLU from scipy.stats import multivariate_normal + from SimPEG import regularization from SimPEG.maps import Wires from SimPEG.utils import WeightedGaussianMixture, mkvc @@ -85,9 +86,7 @@ def test_full_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) - + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -193,8 +192,7 @@ def test_tied_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -297,8 +295,7 @@ def test_diag_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -401,8 +398,7 @@ def test_spherical_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index 83c78f9758..630e45a4d8 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -155,8 +155,10 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2.0 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 return misfit, dmisfit def derChk(m): @@ -314,8 +316,10 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 return misfit, dmisfit def derChk(m): @@ -450,8 +454,10 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 return misfit, dmisfit def derChk(m): diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 78dc40de46..96bd691749 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -463,7 +463,7 @@ def test_quadtree_grav_inverse(self): self.assertAlmostEqual(model_residual, 0.1, delta=0.1) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.grav_inv.invProb.dmisfit(self.grav_model) + data_misfit = self.grav_inv.invProb.dmisfit(self.grav_model) self.assertLess(data_misfit, dpred.shape[0] * 1.15) def test_quadtree_mag_inverse(self): @@ -481,7 +481,7 @@ def test_quadtree_mag_inverse(self): self.assertAlmostEqual(model_residual, 0.01, delta=0.05) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.mag_inv.invProb.dmisfit(self.mag_model) + data_misfit = self.mag_inv.invProb.dmisfit(self.mag_model) self.assertLess(data_misfit, dpred.shape[0] * 1.1) def test_quadtree_grav_inverse_activecells(self): @@ -501,7 +501,7 @@ def test_quadtree_grav_inverse_activecells(self): self.assertAlmostEqual(model_residual, 0.1, delta=0.1) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.grav_inv_active.invProb.dmisfit( + data_misfit = self.grav_inv_active.invProb.dmisfit( self.grav_model[self.active_cells] ) self.assertLess(data_misfit, dpred.shape[0] * 1.1) @@ -530,7 +530,7 @@ def test_quadtree_mag_inverse_activecells(self): self.assertAlmostEqual(model_residual, 0.01, delta=0.05) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.mag_inv_active.invProb.dmisfit( + data_misfit = self.mag_inv_active.invProb.dmisfit( self.mag_model[self.active_cells] ) self.assertLess(data_misfit, dpred.shape[0] * 1.1) diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 9fc7018435..30655046b0 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -75,7 +75,7 @@ def g(k): def test_dm_eigenvalue_by_power_iteration(self): # Test for a single data misfit - dmis_matrix = self.G.T.dot((self.dmis.W**2).dot(self.G)) + dmis_matrix = 2 * self.G.T.dot((self.dmis.W**2).dot(self.G)) field = self.dmis.simulation.fields(self.true_model) max_eigenvalue_numpy, _ = eigsh(dmis_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( @@ -89,7 +89,7 @@ def test_dm_eigenvalue_by_power_iteration(self): WtW = 0.0 for mult, dm in zip(self.dmiscombo.multipliers, self.dmiscombo.objfcts): WtW += mult * dm.W**2 - dmiscombo_matrix = self.G.T.dot(WtW.dot(self.G)) + dmiscombo_matrix = 2 * self.G.T.dot(WtW.dot(self.G)) max_eigenvalue_numpy, _ = eigsh(dmiscombo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( self.dmiscombo, self.true_model, n_pw_iter=30 @@ -110,7 +110,7 @@ def test_reg_eigenvalue_by_power_iteration(self): def test_combo_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) - dmis_matrix = self.G.T.dot((self.dmis.W**2).dot(self.G)) + dmis_matrix = 2 * self.G.T.dot((self.dmis.W**2).dot(self.G)) combo_matrix = dmis_matrix + self.beta * reg_maxtrix max_eigenvalue_numpy, _ = eigsh(combo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( From b22e6a2069bbcf37dc9331e6b703750767af0895 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 22 Feb 2024 11:26:52 -0800 Subject: [PATCH 351/455] Improvements to template for a bug report issue (#1359) Fix typos in the template for the bug report issue, and simplify the text. --- .github/ISSUE_TEMPLATE/bug-report.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 5e2e876ec4..ba93c01385 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,7 +7,7 @@ body: - type: markdown attributes: value: > - Thanks for your use of SimPEG and for taking the time to report a bug! Please + Thanks for using SimPEG and taking the time to report a bug! Please first double check that there is not already a bug report on this issue by searching through the existing bugs. @@ -19,11 +19,11 @@ body: - type: textarea attributes: - label: "Reproducable code example:" + label: "Reproducible code example:" description: > Please submit a small, but complete, code sample that reproduces the bug or missing functionality. It should be able to be copy-pasted - into a Python interpreter and ran as-is. + into a Python interpreter and run as-is. placeholder: | import SimPEG << your code here >> @@ -58,4 +58,4 @@ body: placeholder: | << your explanation here >> validations: - required: false \ No newline at end of file + required: false From ff5df80669cc9efef68f680a42936c86f51d3700 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sun, 25 Feb 2024 16:46:12 -0800 Subject: [PATCH 352/455] fix sizes on creation of empty G matrix --- SimPEG/simulation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index 656645cc88..cc1b172b5f 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -665,12 +665,13 @@ def G(self): Matrix whose rows are the kernel functions """ if getattr(self, "_G", None) is None: - G = np.empty((self.mesh.nC, self.n_kernels)) + G_nodes = np.empty((self.mesh.n_nodes, self.n_kernels)) for i in range(self.n_kernels): - G[:, i] = self.g(i) + print(self.g(i).shape) + G_nodes[:, i] = self.g(i) - self._G = ( - sdiag(self.mesh.cell_volumes) @ (self.mesh.average_node_to_cell @ G).T + self._G = (self.mesh.average_node_to_cell @ G_nodes).T @ sdiag( + self.mesh.cell_volumes ) return self._G From ec7d62957bcb3ec033d8758b5a62a1f0cbc61281 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 28 Feb 2024 09:44:49 -0800 Subject: [PATCH 353/455] Simplify a few gravity simulation tests (#1363) Remove some unneeded lines in gravity simulation tests, particularly the ones that tests warnings and errors being raised. Replace `unittest.mock.patch` with `pytest`'s Monkeypatch to fake `choclo` not being installed. --- tests/pf/test_forward_Grav_Linear.py | 86 ++++------------------------ 1 file changed, 11 insertions(+), 75 deletions(-) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 496e80b1f7..7208f0f9e3 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,6 +1,6 @@ -from unittest.mock import patch import pytest import discretize +import SimPEG from SimPEG import maps from SimPEG.potential_fields import gravity from geoana.gravity import Prism @@ -314,87 +314,38 @@ def test_sensitivity_dtype( assert simulation.sensitivity_dtype is np.float32 @pytest.mark.parametrize("invalid_dtype", (float, np.float16)) - def test_invalid_sensitivity_dtype_assignment( - self, simple_mesh, receivers_locations, invalid_dtype - ): + def test_invalid_sensitivity_dtype_assignment(self, simple_mesh, invalid_dtype): """ Test invalid sensitivity_dtype assignment """ - # Create survey - receivers = gravity.Point(receivers_locations, components="gz") - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Create reduced identity map for Linear Problem - active_cells = np.ones(simple_mesh.n_cells, dtype=bool) - idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) - # Create simulation simulation = gravity.Simulation3DIntegral( simple_mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, ) # Check if error is raised msg = "sensitivity_dtype must be either np.float32 or np.float64." with pytest.raises(TypeError, match=msg): simulation.sensitivity_dtype = invalid_dtype - def test_invalid_engine(self, simple_mesh, receivers_locations): + def test_invalid_engine(self, simple_mesh): """Test if error is raised after invalid engine.""" - # Create survey - receivers = gravity.Point(receivers_locations, components="gz") - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Create reduced identity map for Linear Problem - active_cells = np.ones(simple_mesh.n_cells, dtype=bool) - idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) - # Check if error is raised after an invalid engine is passed engine = "invalid engine" with pytest.raises(ValueError, match=f"Invalid engine '{engine}'"): - gravity.Simulation3DIntegral( - simple_mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - engine=engine, - ) + gravity.Simulation3DIntegral(simple_mesh, engine=engine) - def test_choclo_and_n_proceesses(self, simple_mesh, receivers_locations): + def test_choclo_and_n_proceesses(self, simple_mesh): """Check if warning is raised after passing n_processes with choclo engine.""" - # Create survey - receivers = gravity.Point(receivers_locations, components="gz") - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Create reduced identity map for Linear Problem - active_cells = np.ones(simple_mesh.n_cells, dtype=bool) - idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) - # Check if warning is raised msg = "The 'n_processes' will be ignored when selecting 'choclo'" with pytest.warns(UserWarning, match=msg): simulation = gravity.Simulation3DIntegral( - simple_mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - engine="choclo", - n_processes=2, + simple_mesh, engine="choclo", n_processes=2 ) # Check if n_processes was overwritten and set to None assert simulation.n_processes is None - def test_choclo_and_sensitivity_path_as_dir( - self, simple_mesh, receivers_locations, tmp_path - ): + def test_choclo_and_sensitivity_path_as_dir(self, simple_mesh, tmp_path): """ Check if error is raised when sensitivity_path is a dir with choclo engine. """ - # Create survey - receivers = gravity.Point(receivers_locations, components="gz") - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Create reduced identity map for Linear Problem - active_cells = np.ones(simple_mesh.n_cells, dtype=bool) - idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) # Create a sensitivity_path directory sensitivity_path = tmp_path / "sensitivity_dummy" sensitivity_path.mkdir() @@ -403,36 +354,21 @@ def test_choclo_and_sensitivity_path_as_dir( with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral( simple_mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, store_sensitivities="disk", sensitivity_path=str(sensitivity_path), engine="choclo", ) - @patch("SimPEG.potential_fields.gravity.simulation.choclo", None) - def test_choclo_missing(self, simple_mesh, receivers_locations): + def test_choclo_missing(self, simple_mesh, monkeypatch): """ Check if error is raised when choclo is missing and chosen as engine. """ - # Create survey - receivers = gravity.Point(receivers_locations, components="gz") - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Create reduced identity map for Linear Problem - active_cells = np.ones(simple_mesh.n_cells, dtype=bool) - idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Monkeypatch choclo in SimPEG.potential_fields.base + monkeypatch.setattr(SimPEG.potential_fields.gravity.simulation, "choclo", None) # Check if error is raised msg = "The choclo package couldn't be found." with pytest.raises(ImportError, match=msg): - gravity.Simulation3DIntegral( - simple_mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - engine="choclo", - ) + gravity.Simulation3DIntegral(simple_mesh, engine="choclo") class TestConversionFactor: From 10add2d334889df3d255b04275a2afa90b61a9c6 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 28 Feb 2024 14:34:52 -0800 Subject: [PATCH 354/455] remove print statement --- SimPEG/simulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index cc1b172b5f..c9dccd53d1 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -668,7 +668,6 @@ def G(self): G_nodes = np.empty((self.mesh.n_nodes, self.n_kernels)) for i in range(self.n_kernels): - print(self.g(i).shape) G_nodes[:, i] = self.g(i) self._G = (self.mesh.average_node_to_cell @ G_nodes).T @ sdiag( From d993a0f361010e88ed4edf6dab1aa3f52850e0bf Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 28 Feb 2024 14:39:23 -0800 Subject: [PATCH 355/455] add documentation stating what a datum is and how we evaluate the integral --- SimPEG/simulation.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index c9dccd53d1..da0c94b1cb 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -557,11 +557,20 @@ def Jtvec(self, m, v, f=None): class ExponentialSinusoidSimulation(LinearSimulation): r""" This is the simulation class for the linear problem consisting of - exponentially decaying sinusoids. The rows of the G matrix are + exponentially decaying sinusoids. The kernel functions take the form: .. math:: \int_x e^{p j_k x} \cos(\pi q j_k x) \quad, j_k \in [j_0, ..., j_n] + + The model is defined at cell centers while the kernel functions are defined on nodes. + The trapezoid rule is used to evaluate the integral + + .. math:: + + d_j = \int g_j(x) m(x) dx + + to define our data. """ def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): From 0d9adeebe2b4f9d7e0f482016df9e71421ba9888 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 12:24:00 -0800 Subject: [PATCH 356/455] Raise error when using magnetic's SourceField Make `UniformBackgroundField` to raise an error if `parameters` is being passed to the constructor and add test for it. --- SimPEG/potential_fields/magnetics/sources.py | 23 ++++++++++++++----- tests/pf/test_mag_uniform_background_field.py | 19 +++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 tests/pf/test_mag_uniform_background_field.py diff --git a/SimPEG/potential_fields/magnetics/sources.py b/SimPEG/potential_fields/magnetics/sources.py index 6c9d13c50a..397d310f60 100644 --- a/SimPEG/potential_fields/magnetics/sources.py +++ b/SimPEG/potential_fields/magnetics/sources.py @@ -26,11 +26,22 @@ class UniformBackgroundField(BaseSrc): def __init__( self, receiver_list=None, - amplitude=50000, - inclination=90, - declination=0, - **kwargs + amplitude=50000.0, + inclination=90.0, + declination=0.0, + **kwargs, ): + # Raise errors on 'parameters' argument + # The parameters argument was supported in the deprecated SourceField + # class. We would like to raise an error in case the user passes it + # so the class doesn't behave differently than expected. + if (key := "parameters") in kwargs: + raise TypeError( + f"'{key}' property has been removed." + "Please pass the amplitude, inclination and declination" + " through their own arguments." + ) + self.amplitude = amplitude self.inclination = inclination self.declination = declination @@ -39,7 +50,7 @@ def __init__( @property def amplitude(self): - """Amplitude of the inducing backgound field. + """Amplitude of the inducing background field. Returns ------- @@ -92,7 +103,7 @@ def b0(self): ) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", future_warn=True, error=True) class SourceField(UniformBackgroundField): """Source field for magnetics integral formulation diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py new file mode 100644 index 0000000000..df7cc5ed76 --- /dev/null +++ b/tests/pf/test_mag_uniform_background_field.py @@ -0,0 +1,19 @@ +""" +Test the UniformBackgroundField class +""" +import pytest +from SimPEG.potential_fields.magnetics import UniformBackgroundField + + +def test_invalid_parameters_argument(): + """ + Test if error is raised after passing 'parameters' as argument + """ + parameters = (1, 35, 60) + msg = ( + "'parameters' property has been removed." + "Please pass the amplitude, inclination and declination" + " through their own arguments." + ) + with pytest.raises(TypeError, match=msg): + UniformBackgroundField(parameters=parameters) From 507ed61b81a1e8398084805d3d2606c10205c7f6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 12:25:30 -0800 Subject: [PATCH 357/455] Update docstring in magnetic survey --- SimPEG/potential_fields/magnetics/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimPEG/potential_fields/magnetics/survey.py b/SimPEG/potential_fields/magnetics/survey.py index beed236268..98ac827a5c 100644 --- a/SimPEG/potential_fields/magnetics/survey.py +++ b/SimPEG/potential_fields/magnetics/survey.py @@ -9,7 +9,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.magnetics.sources.SourceField + source_field : SimPEG.potential_fields.magnetics.sources.UniformBackgroundField A source object that defines the Earth's inducing field """ From e2b92772102643849c10353fedbaf30283dedd8e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 13:44:56 -0800 Subject: [PATCH 358/455] Update tests/pf to use the UniformBackgroundField --- tests/pf/test_forward_Mag_Linear.py | 36 ++++++++++++------- tests/pf/test_forward_PFproblem.py | 8 +++-- tests/pf/test_mag_MVI_Octree.py | 9 +++-- tests/pf/test_mag_inversion_linear.py | 9 +++-- tests/pf/test_mag_inversion_linear_Octree.py | 9 +++-- tests/pf/test_mag_nonLinear_Amplitude.py | 16 ++++++--- tests/pf/test_mag_vector_amplitude.py | 9 +++-- tests/pf/test_pf_quadtree_inversion_linear.py | 27 ++++++++++---- tests/pf/test_sensitivity_PFproblem.py | 2 +- 9 files changed, 91 insertions(+), 34 deletions(-) diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 662556b93e..c3125b6499 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -13,8 +13,8 @@ def test_ana_mag_forward(): nx = 5 ny = 5 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) chi1 = 0.01 chi2 = 0.02 @@ -62,7 +62,10 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) @@ -219,8 +222,8 @@ def test_ana_mag_grad_forward(): nx = 5 ny = 5 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) chi1 = 0.01 chi2 = 0.02 @@ -268,7 +271,10 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + [rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) @@ -315,8 +321,8 @@ def test_ana_mag_vec_forward(): nx = 5 ny = 5 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 @@ -362,7 +368,10 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) @@ -403,8 +412,8 @@ def test_ana_mag_amp_forward(): nx = 5 ny = 5 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 @@ -450,7 +459,10 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 65793bb650..b0914da781 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -12,7 +12,6 @@ def setUp(self): Inc = 45.0 Dec = 45.0 Btot = 51000 - H0 = (Btot, Inc, Dec) self.b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) @@ -40,7 +39,12 @@ def setUp(self): self.yr = yr self.rxLoc = np.c_[utils.mkvc(X), utils.mkvc(Y), utils.mkvc(Z)] receivers = mag.Point(self.rxLoc, components=components) - srcField = mag.SourceField([receivers], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=Btot, + inclination=Inc, + declination=Dec, + ) self.survey = mag.Survey(srcField) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 49809e4c7d..537c4b1720 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -20,7 +20,7 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): np.random.seed(0) - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different # direction (induced + remanence) @@ -46,7 +46,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create a mesh diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 868ad8b34b..611935adf4 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -23,7 +23,7 @@ def setUp(self): np.random.seed(0) # Define the inducing field parameter - H0 = (50000, 90, 0) + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a mesh dx = 5.0 @@ -59,7 +59,12 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(rxLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # We can now create a susceptibility model and generate data diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index d30e8a7184..64ce31e0bd 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -26,7 +26,7 @@ def setUp(self): # From old convention, field orientation is given as an # azimuth from North (positive clockwise) # and dip from the horizontal (positive downward). - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh h = [5, 5, 5] @@ -55,7 +55,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # self.mesh.finalize() diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index 318964328f..186fdc1343 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -21,7 +21,7 @@ class AmpProblemTest(unittest.TestCase): def setUp(self): # We will assume a vertical inducing field - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -46,8 +46,11 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(srcField) @@ -185,8 +188,11 @@ def setUp(self): # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) surveyAmp = magnetics.survey.Survey(srcField) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 84d56ee320..af4619e14f 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -20,7 +20,7 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): np.random.seed(0) - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different # direction (induced + remanence) @@ -46,7 +46,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create a mesh diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 96bd691749..46bcf77c67 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -104,9 +104,14 @@ def create_gravity_sim_flat(self, block_value=1.0, noise_floor=0.01): def create_magnetics_sim_flat(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz_flat) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + [mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator @@ -159,9 +164,14 @@ def create_gravity_sim(self, block_value=1.0, noise_floor=0.01): def create_magnetics_sim(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + [mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator @@ -215,9 +225,14 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + receiver_list=[mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index 99d5fb4f37..53c96c96cb 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -41,7 +41,7 @@ # # components = ['bx', 'by', 'bz'] # receivers = mag.Point(rxLoc, components=components) -# srcField = mag.SourceField([receivers], parameters=H0) +# srcField = mag.UniformBackgroundField([receivers], parameters=H0) # # self.survey = mag.Survey(srcField) # From 2c0f6423ee3a2767aac00f51b614216c995c8c5c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 13:49:38 -0800 Subject: [PATCH 359/455] Update other tests to use UniformBackgroundField --- tests/base/test_directives.py | 9 +++++++-- tests/dask/test_mag_MVI_Octree.py | 9 +++++++-- tests/dask/test_mag_inversion_linear_Octree.py | 9 +++++++-- tests/dask/test_mag_nonLinear_Amplitude.py | 16 +++++++++++----- tests/utils/test_io_utils.py | 11 +++++++---- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 8637e633af..6a939d9f7b 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -64,11 +64,16 @@ def setUp(self): mesh = discretize.TensorMesh([4, 4, 4]) # Magnetic inducing field parameter (A,I,D) - B = [50000, 90, 0] + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a MAGsurvey rx = mag.Point(np.vstack([[0.25, 0.25, 0.25], [-0.25, -0.25, 0.25]])) - srcField = mag.UniformBackgroundField([rx], parameters=(B[0], B[1], B[2])) + srcField = mag.UniformBackgroundField( + receiver_list=[rx], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create the forward model operator diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index e7e5699224..7189e99df4 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -21,7 +21,7 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): np.random.seed(0) - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different # direction (induced + remanence) @@ -47,7 +47,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create a mesh diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index cf16cb1578..098423b4f8 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -29,7 +29,7 @@ def setUp(self): # From old convention, field orientation is given as an # azimuth from North (positive clockwise) # and dip from the horizontal (positive downward). - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh h = [5, 5, 5] @@ -58,7 +58,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) self.mesh = mesh_utils.mesh_builder_xyz( diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 0118ac78f9..5b0dfdf6f6 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -22,7 +22,7 @@ class AmpProblemTest(unittest.TestCase): def setUp(self): # We will assume a vertical inducing field - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -47,8 +47,11 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(srcField) @@ -186,8 +189,11 @@ def setUp(self): # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) surveyAmp = magnetics.survey.Survey(srcField) diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 54e6282fe0..76159b6a6a 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -242,9 +242,12 @@ def setUp(self): xyz = np.c_[x, y, z] rx = magnetics.receivers.Point(xyz, components="tmi") - inducing_field = (50000.0, 60.0, 15.0) - source_field = magnetics.sources.SourceField( - receiver_list=rx, parameters=inducing_field + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 15.0) + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=rx, + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(source_field) @@ -253,7 +256,7 @@ def setUp(self): self.std = std rx2 = magnetics.receivers.Point(xyz, components="tmi") - src_bad = magnetics.sources.SourceField([rx, rx2]) + src_bad = magnetics.sources.UniformBackgroundField([rx, rx2]) survey_bad = magnetics.survey.Survey(src_bad) self.survey_bad = survey_bad From 5ca010a46e547964b2366c0454545dce65741593 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 13:56:35 -0800 Subject: [PATCH 360/455] Update examples and tutorials --- examples/01-maps/plot_sumMap.py | 9 +++++++-- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 9 +++++++-- .../plot_inv_mag_MVI_VectorAmplitude.py | 9 +++++++-- .../plot_inv_mag_nonLinear_Amplitude.py | 16 +++++++++++++--- examples/_archived/plot_inv_mag_linear.py | 9 +++++++-- .../04-magnetics/plot_2a_magnetics_induced.py | 8 +++++--- tutorials/04-magnetics/plot_2b_magnetics_mvi.py | 8 +++++--- .../plot_inv_2a_magnetics_induced.py | 8 +++++--- .../plot_inv_3_cross_gradient_pf.py | 8 +++++--- 9 files changed, 61 insertions(+), 23 deletions(-) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index 270e7cec22..dd7a1d012b 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -30,7 +30,7 @@ def run(plotIt=True): - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh dx = 5.0 @@ -62,7 +62,12 @@ def run(plotIt=True): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = magnetics.Point(rxLoc) - srcField = magnetics.SourceField([rxLoc], parameters=H0) + srcField = magnetics.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = magnetics.Survey(srcField) # We can now create a susceptibility model and generate data diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 9c420650b6..7cc54915f2 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -51,7 +51,7 @@ # np.random.seed(1) # We will assume a vertical inducing field -H0 = (50000.0, 90.0, 0.0) +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -74,7 +74,12 @@ # Create a MAGsurvey xyzLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] rxLoc = magnetics.receivers.Point(xyzLoc) -srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) survey = magnetics.survey.Survey(srcField) # Here how the topography looks with a quick interpolation, just a Gaussian... diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index bc23e82d3c..0e8740197d 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -44,7 +44,7 @@ # np.random.seed(1) # We will assume a vertical inducing field -H0 = (50000.0, 90.0, 0.0) +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create grid of points for topography # Lets create a simple Gaussian topo and set the active cells @@ -63,7 +63,12 @@ # Create a MAGsurvey xyzLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] rxLoc = magnetics.receivers.Point(xyzLoc) -srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) survey = magnetics.survey.Survey(srcField) ############################################################################### diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index 3f43150103..8c5cfb1af8 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -50,7 +50,7 @@ # # We will assume a vertical inducing field -H0 = (50000.0, 90.0, 0.0) +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -75,7 +75,12 @@ # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) -srcField = magnetics.sources.SourceField(receiver_list=[receiver_list], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) survey = magnetics.survey.Survey(srcField) # Here how the topography looks with a quick interpolation, just a Gaussian... @@ -267,7 +272,12 @@ # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) -srcField = magnetics.sources.SourceField(receiver_list=[receiver_list], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) surveyAmp = magnetics.survey.Survey(srcField) simulation = magnetics.simulation.Simulation3DIntegral( diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 09bfe42a64..62d5114807 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -26,7 +26,7 @@ def run(plotIt=True): # Define the inducing field parameter - H0 = (50000, 90, 0) + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a mesh dx = 5.0 @@ -64,7 +64,12 @@ def run(plotIt=True): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = magnetics.receivers.Point(rxLoc, components=["tmi"]) - srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = magnetics.survey.Survey(srcField) # We can now create a susceptibility model and generate data diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index 75e8b2bca1..af08030220 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -82,10 +82,12 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=strength, + inclination=inclination, + declination=declination, ) # Define the survey diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index be8f7a63b7..6ffe6ac691 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -81,10 +81,12 @@ field_inclination = 60 field_declination = 30 field_strength = 50000 -inducing_field = (field_strength, field_inclination, field_declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=field_strength, + inclination=field_inclination, + declination=field_declination, ) # Define the survey diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 0b4bb43ec7..07074a525d 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -160,10 +160,12 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=strength, + inclination=inclination, + declination=declination, ) # Define the survey diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 56ef62c72a..27bcfb523e 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -196,11 +196,13 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) # Define the source field and survey for gravity data -source_field_mag = magnetics.sources.SourceField( - receiver_list=[receiver_mag], parameters=inducing_field +source_field_mag = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_mag], + amplitude=strength, + inclination=inclination, + declination=declination, ) survey_mag = magnetics.survey.Survey(source_field_mag) From 6bce940c0fab0e62a1fe506de6f3297fedc6fe8c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 29 Feb 2024 16:24:23 -0800 Subject: [PATCH 361/455] Make sure that SourceField will throw an error Correctly choose the arguments for the `@deprecateclass` decorator and add test checking that creating a `SourceField` object will raise an error. --- SimPEG/potential_fields/magnetics/sources.py | 2 +- tests/pf/test_mag_uniform_background_field.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/SimPEG/potential_fields/magnetics/sources.py b/SimPEG/potential_fields/magnetics/sources.py index 397d310f60..c62bc80b23 100644 --- a/SimPEG/potential_fields/magnetics/sources.py +++ b/SimPEG/potential_fields/magnetics/sources.py @@ -103,7 +103,7 @@ def b0(self): ) -@deprecate_class(removal_version="0.19.0", future_warn=True, error=True) +@deprecate_class(removal_version="0.19.0", error=True) class SourceField(UniformBackgroundField): """Source field for magnetics integral formulation diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index df7cc5ed76..18989d4d09 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -2,7 +2,7 @@ Test the UniformBackgroundField class """ import pytest -from SimPEG.potential_fields.magnetics import UniformBackgroundField +from SimPEG.potential_fields.magnetics import UniformBackgroundField, SourceField def test_invalid_parameters_argument(): @@ -17,3 +17,12 @@ def test_invalid_parameters_argument(): ) with pytest.raises(TypeError, match=msg): UniformBackgroundField(parameters=parameters) + + +def test_deprecated_source_field(): + """ + Test if instantiating a magnetics.source.SourceField object raises an error + """ + msg = "SourceField has been removed, please use UniformBackgroundField." + with pytest.raises(NotImplementedError, match=msg): + SourceField() From bae455a9b10aba6fd32074c60e96d32410495fbe Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 5 Mar 2024 15:19:11 -0800 Subject: [PATCH 362/455] Remove deprecated regularization classes (#1365) Raise errors when constructing deprecated regularization classes. Add test that checks if those errors are being raised. Replace the removed regularization classes that were still being used in a few pieces of the code and in the tests. Use the new classes instead. Add removed classes to the `IGNORE_ME` list in `test_regularization.py` so other tests don't fail. --- SimPEG/directives/directives.py | 18 +-------- SimPEG/directives/pgi_directives.py | 8 +--- .../spectral_induced_polarization/run.py | 10 +++-- SimPEG/regularization/__init__.py | 18 ++++----- tests/base/test_regularization.py | 38 ++++++++++++++++++- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 12 ++++-- 6 files changed, 65 insertions(+), 39 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 6f38db40be..5141bebaf3 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -14,7 +14,6 @@ Sparse, SparseSmallness, PGIsmallness, - PGIwithNonlinearRelationshipsSmallness, SmoothnessFirstOrder, SparseSmoothness, BaseSimilarityMeasure, @@ -716,7 +715,6 @@ def initialize(self): Smallness, SparseSmallness, PGIsmallness, - PGIwithNonlinearRelationshipsSmallness, ), ): smallness += [obj] @@ -1288,13 +1286,7 @@ def initialize(self): np.r_[ i, j, - ( - isinstance( - regpart, - PGIwithNonlinearRelationshipsSmallness, - ) - or isinstance(regpart, PGIsmallness) - ), + isinstance(regpart, PGIsmallness), ] ) for i, regobjcts in enumerate(self.invProb.reg.objfcts) @@ -1332,13 +1324,7 @@ def initialize(self): ( np.r_[ j, - ( - isinstance( - regpart, - PGIwithNonlinearRelationshipsSmallness, - ) - or isinstance(regpart, PGIsmallness) - ), + isinstance(regpart, PGIsmallness), ] ) for j, regpart in enumerate(self.invProb.reg.objfcts) diff --git a/SimPEG/directives/pgi_directives.py b/SimPEG/directives/pgi_directives.py index e8fb543ee1..0cc141f026 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/SimPEG/directives/pgi_directives.py @@ -12,7 +12,6 @@ from ..regularization import ( PGI, PGIsmallness, - PGIwithRelationships, SmoothnessFirstOrder, SparseSmoothness, ) @@ -363,12 +362,7 @@ def initialize(self): if getattr(self.reg.objfcts[0], "objfcts", None) is not None: # Find the petrosmallness terms in a two-levels combo-regularization. petrosmallness = np.where( - np.r_[ - [ - isinstance(regpart, (PGI, PGIwithRelationships)) - for regpart in self.reg.objfcts - ] - ] + np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] )[0][0] self.petrosmallness = petrosmallness diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py b/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py index 18b766b3e1..bf568e31d7 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py +++ b/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py @@ -142,9 +142,13 @@ def run_inversion( m_lower = np.r_[eta_lower, tau_lower, c_lower] # Set up regularization - reg_eta = regularization.Simple(mesh, mapping=wires.eta, indActive=actind) - reg_tau = regularization.Simple(mesh, mapping=wires.tau, indActive=actind) - reg_c = regularization.Simple(mesh, mapping=wires.c, indActive=actind) + reg_eta = regularization.WeightedLeastSquares( + mesh, mapping=wires.eta, indActive=actind + ) + reg_tau = regularization.WeightedLeastSquares( + mesh, mapping=wires.tau, indActive=actind + ) + reg_c = regularization.WeightedLeastSquares(mesh, mapping=wires.c, indActive=actind) # Todo: diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index c379bbd202..334e4ed986 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -171,21 +171,21 @@ ) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class SimpleSmall(Smallness): """Deprecated class, replaced by Smallness.""" pass -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class SimpleSmoothDeriv(SmoothnessFirstOrder): """Deprecated class, replaced by SmoothnessFirstOrder.""" pass -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class Simple(WeightedLeastSquares): """Deprecated class, replaced by WeightedLeastSquares.""" @@ -201,7 +201,7 @@ def __init__(self, mesh=None, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs): ) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class Tikhonov(WeightedLeastSquares): """Deprecated class, replaced by WeightedLeastSquares.""" @@ -218,28 +218,28 @@ def __init__( ) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class Small(Smallness): """Deprecated class, replaced by Smallness.""" pass -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class SmoothDeriv(SmoothnessFirstOrder): """Deprecated class, replaced by SmoothnessFirstOrder.""" pass -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class SmoothDeriv2(SmoothnessSecondOrder): """Deprecated class, replaced by SmoothnessSecondOrder.""" pass -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class PGIwithNonlinearRelationshipsSmallness(PGIsmallness): """Deprecated class, replaced by PGIsmallness.""" @@ -247,7 +247,7 @@ def __init__(self, gmm, **kwargs): super().__init__(gmm, non_linear_relationships=True, **kwargs) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@deprecate_class(removal_version="0.19.0", error=True) class PGIwithRelationships(PGI): """Deprecated class, replaced by PGI.""" diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 6e73479785..18aac616b6 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -39,6 +39,16 @@ "BaseAmplitude", "VectorAmplitude", "CrossReferenceRegularization", + # Removed regularization classes that raise error on instantiation + "PGIwithNonlinearRelationshipsSmallness", + "PGIwithRelationships", + "Simple", + "SimpleSmall", + "SimpleSmoothDeriv", + "Small", + "SmoothDeriv", + "SmoothDeriv2", + "Tikhonov", ] @@ -457,7 +467,7 @@ def test_nC_residual(self): mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * actMap regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) - reg = regularization.Simple(regMesh) + reg = regularization.WeightedLeastSquares(regMesh) self.assertTrue(reg._nC_residual == regMesh.nC) self.assertTrue(all([fct._nC_residual == regMesh.nC for fct in reg.objfcts])) @@ -793,5 +803,31 @@ def test_weights(self, mesh): BaseRegularization(mesh, weights=weights, cell_weights=weights) +class TestRemovedRegularizations: + """ + Test if errors are raised after creating removed regularization classes. + """ + + @pytest.mark.parametrize( + "regularization_class", + ( + regularization.PGIwithNonlinearRelationshipsSmallness, + regularization.PGIwithRelationships, + regularization.Simple, + regularization.SimpleSmall, + regularization.SimpleSmoothDeriv, + regularization.Small, + regularization.SmoothDeriv, + regularization.SmoothDeriv2, + regularization.Tikhonov, + ), + ) + def test_removed_class(self, regularization_class): + class_name = regularization_class.__name__ + msg = f"{class_name} has been removed, please use." + with pytest.raises(NotImplementedError, match=msg): + regularization_class() + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 887aad24db..a299c339f5 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -264,9 +264,15 @@ def setUp(self): dobs = problem.make_synthetic_data(mSynth, add_noise=True) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) - reg_eta = regularization.Simple(mesh, mapping=wires.eta, indActive=~airind) - reg_taui = regularization.Simple(mesh, mapping=wires.taui, indActive=~airind) - reg_c = regularization.Simple(mesh, mapping=wires.c, indActive=~airind) + reg_eta = regularization.WeightedLeastSquares( + mesh, mapping=wires.eta, indActive=~airind + ) + reg_taui = regularization.WeightedLeastSquares( + mesh, mapping=wires.taui, indActive=~airind + ) + reg_c = regularization.WeightedLeastSquares( + mesh, mapping=wires.c, indActive=~airind + ) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 From 7fe2c33736e9632fd9c36296d0f89e32a53dfea5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 6 Mar 2024 11:46:56 -0800 Subject: [PATCH 363/455] Removed deprecated properties of UpdateSensitivityWeights (#1368) #### Summary Remove the `everyIter`, `threshold` and `normalization` properties of `UpdateSensitivityWeights`. Add tests to check if errors are being raised after passing removed arguments to its constructor. Update tests and examples using the removed arguments and properties. #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR * [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). * [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### Reference issue Part of the solution for https://github.com/simpeg/simpeg/issues/1302 #### What does this implement/fix? #### Additional information Most of these changes were cherry-picked from #1306 --- SimPEG/directives/directives.py | 58 ++++------- examples/02-gravity/plot_inv_grav_tiled.py | 2 +- examples/_archived/plot_inv_grav_linear.py | 2 +- examples/_archived/plot_inv_mag_linear.py | 2 +- tests/base/test_directives.py | 96 ++++++++++++++++--- tests/dask/test_grav_inversion_linear.py | 2 +- tests/dask/test_mag_MVI_Octree.py | 2 +- tests/pf/test_mag_MVI_Octree.py | 2 +- tests/pf/test_mag_inversion_linear.py | 2 +- tests/pf/test_mag_vector_amplitude.py | 2 +- .../plot_inv_2_inversion_irls.py | 2 +- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 2 +- .../plot_inv_1b_gravity_anomaly_irls.py | 2 +- .../plot_inv_2a_magnetics_induced.py | 2 +- tutorials/06-ip/plot_inv_2_dcip2d.py | 2 +- tutorials/06-ip/plot_inv_3_dcip3d.py | 2 +- .../plot_inv_3_cross_gradient_pf.py | 2 +- 17 files changed, 118 insertions(+), 66 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 5141bebaf3..23addd7899 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -2521,33 +2521,20 @@ def __init__( normalization_method="maximum", **kwargs, ): - if "everyIter" in kwargs.keys(): - warnings.warn( - "'everyIter' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'every_iteration'.", - stacklevel=2, + # Raise errors on deprecated arguments + if (key := "everyIter") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'every_iteration'.", ) - every_iteration = kwargs.pop("everyIter") - - if "threshold" in kwargs.keys(): - warnings.warn( - "'threshold' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'threshold_value'.", - stacklevel=2, + if (key := "threshold") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'threshold_value'.", ) - threshold_value = kwargs.pop("threshold") - - if "normalization" in kwargs.keys(): - warnings.warn( - "'normalization' property is deprecated and will be removed in SimPEG 0.20.0." + if (key := "normalization") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. " "Please define normalization using 'normalization_method'.", - stacklevel=2, ) - normalization_method = kwargs.pop("normalization") - if normalization_method is True: - normalization_method = "maximum" - else: - normalization_method = None super().__init__(**kwargs) @@ -2574,7 +2561,11 @@ def every_iteration(self, value): self._every_iteration = validate_type("every_iteration", value, bool) everyIter = deprecate_property( - every_iteration, "everyIter", "every_iteration", removal_version="0.20.0" + every_iteration, + "everyIter", + "every_iteration", + removal_version="0.20.0", + error=True, ) @property @@ -2603,7 +2594,11 @@ def threshold_value(self, value): self._threshold_value = validate_float("threshold_value", value, min_val=0.0) threshold = deprecate_property( - threshold_value, "threshold", "threshold_value", removal_version="0.20.0" + threshold_value, + "threshold", + "threshold_value", + removal_version="0.20.0", + error=True, ) @property @@ -2653,18 +2648,6 @@ def normalization_method(self): def normalization_method(self, value): if value is None: self._normalization_method = value - - elif isinstance(value, bool): - warnings.warn( - "Boolean type for 'normalization_method' is deprecated and will be removed in 0.20.0." - "Please use None, 'maximum' or 'minimum'.", - stacklevel=2, - ) - if value: - self._normalization_method = "maximum" - else: - self._normalization_method = None - else: self._normalization_method = validate_string( "normalization_method", value, string_list=["minimum", "maximum"] @@ -2675,6 +2658,7 @@ def normalization_method(self, value): "normalization", "normalization_method", removal_version="0.20.0", + error=True, ) def initialize(self): diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index cc8fe41f78..5ed4cd90e2 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -243,7 +243,7 @@ ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[update_IRLS, sensitivity_weights, betaest, update_Jacobi, saveDict], diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index d84bcc5bd9..26b9cbf680 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -127,7 +127,7 @@ def run(plotIt=True): ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[ diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 62d5114807..2ae4b535e3 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -131,7 +131,7 @@ def run(plotIt=True): saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() # Add sensitivity weights - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 6a939d9f7b..e4857d08f0 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -133,21 +133,12 @@ def test_validation_in_inversion(self): inv = inversion.BaseInversion(invProb) inv.directiveList = [update_Jacobi, sensitivity_weights] - def test_sensitivity_weighting_warnings(self): - # Test setter warnings - d_temp = directives.UpdateSensitivityWeights() - d_temp.normalization_method = True - self.assertTrue(d_temp.normalization_method == "maximum") - - d_temp.normalization_method = False - self.assertTrue(d_temp.normalization_method is None) - def test_sensitivity_weighting_global(self): test_inputs = { - "everyIter": False, - "threshold": 1e-12, + "every_iteration": False, + "threshold_value": 1e-12, "threshold_method": "global", - "normalization": False, + "normalization_method": None, } # Compute test weights @@ -155,7 +146,7 @@ def test_sensitivity_weighting_global(self): np.sqrt(np.sum((self.dmis.W * self.sim.G) ** 2, axis=0)) / self.mesh.cell_volumes ) - test_weights = sqrt_diagJtJ + test_inputs["threshold"] + test_weights = sqrt_diagJtJ + test_inputs["threshold_value"] test_weights *= self.mesh.cell_volumes # Test directive @@ -182,7 +173,7 @@ def test_sensitivity_weighting_percentile_maximum(self): "every_iteration": True, "threshold_value": 1, "threshold_method": "percentile", - "normalization": True, + "normalization_method": "maximum", } # Compute test weights @@ -303,5 +294,82 @@ def test_save_output_dict(RegClass): assert "x SparseSmoothness.norm" in out_dict +class TestUpdateSensitivityWeightsRemovedArgs: + """ + Test if `UpdateSensitivityWeights` raises errors after passing removed arguments. + """ + + def test_every_iter(self): + """ + Test if `UpdateSensitivityWeights` raises error after passing `everyIter`. + """ + msg = "'everyIter' property has been removed. Please use 'every_iteration'." + with pytest.raises(TypeError, match=msg): + directives.UpdateSensitivityWeights(everyIter=True) + + def test_threshold(self): + """ + Test if `UpdateSensitivityWeights` raises error after passing `threshold`. + """ + msg = "'threshold' property has been removed. Please use 'threshold_value'." + with pytest.raises(TypeError, match=msg): + directives.UpdateSensitivityWeights(threshold=True) + + def test_normalization(self): + """ + Test if `UpdateSensitivityWeights` raises error after passing `normalization`. + """ + msg = ( + "'normalization' property has been removed. " + "Please define normalization using 'normalization_method'." + ) + with pytest.raises(TypeError, match=msg): + directives.UpdateSensitivityWeights(normalization=True) + + +class TestUpdateSensitivityNormalization: + """ + Test the `normalization` property and setter in `UpdateSensitivityWeights` + """ + + @pytest.mark.parametrize("normalization_method", (None, "maximum", "minimum")) + def test_normalization_method_setter_valid(self, normalization_method): + """ + Test if the setter method for normalization_method in + `UpdateSensitivityWeights` works as expected on valid values. + + The `normalization_method` must be a string or a None. This test was + included as part of the removal process of the old `normalization` + property. + """ + d_temp = directives.UpdateSensitivityWeights() + # Use the setter method to assign a value to normalization_method + d_temp.normalization_method = normalization_method + assert d_temp.normalization_method == normalization_method + + @pytest.mark.parametrize("normalization_method", (True, False, "an invalid method")) + def test_normalization_method_setter_invalid(self, normalization_method): + """ + Test if the setter method for normalization_method in + `UpdateSensitivityWeights` raises error on invalid values. + + The `normalization_method` must be a string or a None. This test was + included as part of the removal process of the old `normalization` + property. + """ + d_temp = directives.UpdateSensitivityWeights() + if isinstance(normalization_method, bool): + error_type = TypeError + msg = "'normalization_method' must be a str. Got" + else: + error_type = ValueError + msg = ( + r"'normalization_method' must be in \['minimum', 'maximum'\]. " + f"Got '{normalization_method}'" + ) + with pytest.raises(error_type, match=msg): + d_temp.normalization_method = normalization_method + + if __name__ == "__main__": unittest.main() diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index 9e91433d36..8d35014b9f 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -105,7 +105,7 @@ def setUp(self): # Here is where the norms are applied IRLS = directives.Update_IRLS(max_irls_iterations=20, chifact_start=2.0) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( invProb, directiveList=[IRLS, sensitivity_weights, update_Jacobi] ) diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 7189e99df4..8f9690f216 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -152,7 +152,7 @@ def setUp(self): # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 537c4b1720..1880095af8 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -149,7 +149,7 @@ def setUp(self): # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 611935adf4..c9e0cbc7c3 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -119,7 +119,7 @@ def setUp(self): # Here is where the norms are applied IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=1) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( invProb, directiveList=[IRLS, sensitivity_weights, betaest, update_Jacobi] ) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index af4619e14f..5dea5ded25 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -141,7 +141,7 @@ def setUp(self): # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index 19ae156e89..15c7f18dc5 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -172,7 +172,7 @@ def g(k): # # Add sensitivity weights but don't update at each beta -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # Reach target misfit for L2 solution, then use IRLS until model stops changing. IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-4) diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 3b0c46dcfe..e1e99ebcb7 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -268,7 +268,7 @@ target_misfit = directives.TargetMisfit(chifact=1) # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index fdcca19bc8..23336dee7c 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -274,7 +274,7 @@ update_jacobi = directives.UpdatePreconditioner() # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 07074a525d..7ff2f45463 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -309,7 +309,7 @@ target_misfit = directives.TargetMisfit(chifact=1) # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index 2215462ffd..d4d00efa06 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -567,7 +567,7 @@ # Here we define the directives in the same manner as the DC inverse problem. # -update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold=1e-3) +update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold_value=1e-3) starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) save_iteration = directives.SaveOutputEveryIteration(save_txt=False) diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index 9193d829b7..1be280c5d7 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -633,7 +633,7 @@ # Here we define the directives in the same manner as the DC inverse problem. # -update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold=1e-3) +update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold_value=1e-3) starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e2) beta_schedule = directives.BetaSchedule(coolingFactor=2.5, coolingRate=1) save_iteration = directives.SaveOutputEveryIteration(save_txt=False) diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 27bcfb523e..261af3baf2 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -365,7 +365,7 @@ stopping = directives.MovingAndMultiTargetStopping(tol=1e-6) -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() From 887cfe20e78bb7e8f552911eb5d4d0b510a24b57 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 11 Mar 2024 11:41:28 -0700 Subject: [PATCH 364/455] Replace indActive for active_cells in regularizations (#1366) #### Summary Remove the `indActive` argument in constructors of regularization classes, remove the `indActive` attribute and replace them for the supported `active_cells`. Replace the `indActive` argument in the `depth_weighting` function in favor of `active_cells`. Raise errors when those arguments are being passed and add tests checking those errors are being raised. Update code, tests and examples that were still using `indActive`. #### Reference issue Part of the solution for #1302 Most of these changes were cherry-picked from #1306 --- .../static/induced_polarization/run.py | 4 +- .../static/resistivity/run.py | 4 +- .../spectral_induced_polarization/run.py | 8 ++- SimPEG/regularization/base.py | 41 ++++-------- SimPEG/utils/model_utils.py | 10 +-- .../plot_inv_mag_nonLinear_Amplitude.py | 4 +- examples/08-vrm/plot_inv_vrm_eq.py | 2 +- ...lot_inv_dcip_dipoledipole_2_5Dinversion.py | 2 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 2 +- examples/_archived/plot_inv_grav_linear.py | 2 +- examples/_archived/plot_inv_mag_linear.py | 2 +- tests/base/test_correspondance.py | 2 +- tests/base/test_cross_gradient.py | 12 ++-- tests/base/test_jtv.py | 14 +++-- tests/base/test_model_utils.py | 35 +++++++++-- tests/base/test_regularization.py | 63 +++++++++++++++---- tests/dask/test_mag_MVI_Octree.py | 12 ++-- tests/dask/test_mag_nonLinear_Amplitude.py | 4 +- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 6 +- tests/em/static/test_SIP_jvecjtvecadj.py | 6 +- tests/pf/test_mag_inversion_linear.py | 2 +- tests/pf/test_mag_nonLinear_Amplitude.py | 4 +- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 4 +- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 2 +- tutorials/05-dcr/plot_inv_3_dcr3d.py | 2 +- tutorials/06-ip/plot_inv_2_dcip2d.py | 4 +- tutorials/06-ip/plot_inv_3_dcip3d.py | 4 +- .../plot_inv_3_cross_gradient_pf.py | 6 +- .../_temporary/plot_4c_fdem3d_inversion.py | 2 +- 29 files changed, 159 insertions(+), 106 deletions(-) diff --git a/SimPEG/electromagnetics/static/induced_polarization/run.py b/SimPEG/electromagnetics/static/induced_polarization/run.py index 550579a319..53fbd717f3 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/run.py +++ b/SimPEG/electromagnetics/static/induced_polarization/run.py @@ -37,7 +37,7 @@ def run_inversion( regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion if use_sensitivity_weight: - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) reg.alpha_s = alpha_s reg.alpha_x = alpha_x reg.alpha_y = alpha_y @@ -45,7 +45,7 @@ def run_inversion( else: reg = regularization.Sparse( mesh, - indActive=actind, + active_cells=actind, mapping=regmap, cell_weights=mesh.cell_volumes[actind], ) diff --git a/SimPEG/electromagnetics/static/resistivity/run.py b/SimPEG/electromagnetics/static/resistivity/run.py index a9a04dc3e0..0ee948ea48 100644 --- a/SimPEG/electromagnetics/static/resistivity/run.py +++ b/SimPEG/electromagnetics/static/resistivity/run.py @@ -37,14 +37,14 @@ def run_inversion( regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion if use_sensitivity_weight: - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) reg.alpha_s = alpha_s reg.alpha_x = alpha_x reg.alpha_y = alpha_y reg.alpha_z = alpha_z else: reg = regularization.WeightedLeastSquares( - mesh, indActive=actind, mapping=regmap + mesh, active_cells=actind, mapping=regmap ) reg.alpha_s = alpha_s reg.alpha_x = alpha_x diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py b/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py index bf568e31d7..101385a70b 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py +++ b/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py @@ -143,12 +143,14 @@ def run_inversion( # Set up regularization reg_eta = regularization.WeightedLeastSquares( - mesh, mapping=wires.eta, indActive=actind + mesh, mapping=wires.eta, active_cells=actind ) reg_tau = regularization.WeightedLeastSquares( - mesh, mapping=wires.tau, indActive=actind + mesh, mapping=wires.tau, active_cells=actind + ) + reg_c = regularization.WeightedLeastSquares( + mesh, mapping=wires.c, active_cells=actind ) - reg_c = regularization.WeightedLeastSquares(mesh, mapping=wires.c, indActive=actind) # Todo: diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 4857a376d2..ca38be2e3f 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -67,22 +67,13 @@ def __init__( f"Value of type {type(mesh)} provided." ) - # Handle deprecated indActive argument + # Raise errors on deprecated arguments: avoid old code that still uses + # them to silently fail if (key := "indActive") in kwargs: - if active_cells is not None: - raise ValueError( - f"Cannot simultaneously pass 'active_cells' and '{key}'. " - "Pass 'active_cells' only." - ) - warnings.warn( - f"The '{key}' argument has been deprecated, please use 'active_cells'. " - "It will be removed in future versions of SimPEG.", - DeprecationWarning, - stacklevel=2, + raise TypeError( + f"'{key}' argument has been removed. " + "Please use 'active_cells' instead." ) - active_cells = kwargs.pop(key) - - # Handle deprecated cell_weights argument if (key := "cell_weights") in kwargs: if weights is not None: raise ValueError( @@ -146,8 +137,7 @@ def active_cells(self, values: np.ndarray | None): "indActive", "active_cells", "0.19.0", - future_warn=True, - error=False, + error=True, ) @property @@ -1585,19 +1575,13 @@ def __init__( ) self._regularization_mesh = mesh + # Raise errors on deprecated arguments: avoid old code that still uses + # them to silently fail if (key := "indActive") in kwargs: - if active_cells is not None: - raise ValueError( - f"Cannot simultaneously pass 'active_cells' and '{key}'. " - "Pass 'active_cells' only." - ) - warnings.warn( - f"The '{key}' argument has been deprecated, please use 'active_cells'. " - "It will be removed in future versions of SimPEG.", - DeprecationWarning, - stacklevel=2, + raise TypeError( + f"'{key}' argument has been removed. " + "Please use 'active_cells' instead." ) - active_cells = kwargs.pop(key) self.alpha_s = alpha_s if alpha_x is not None: @@ -2107,8 +2091,7 @@ def active_cells(self, values: np.ndarray): "indActive", "active_cells", "0.19.0", - error=False, - future_warn=True, + error=True, ) @property diff --git a/SimPEG/utils/model_utils.py b/SimPEG/utils/model_utils.py index 8c6d19b1ab..2bdd99b42a 100644 --- a/SimPEG/utils/model_utils.py +++ b/SimPEG/utils/model_utils.py @@ -160,14 +160,10 @@ def depth_weighting( value. """ - if "indActive" in kwargs: - warnings.warn( - "The indActive keyword argument has been deprecated, please use active_cells. " - "This will be removed in SimPEG 0.19.0", - FutureWarning, - stacklevel=2, + if (key := "indActive") in kwargs: + raise TypeError( + f"'{key}' argument has been removed. " "Please use 'active_cells' instead." ) - active_cells = kwargs["indActive"] # Default threshold value if threshold is None: diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index 8c5cfb1af8..bbcd745dce 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -233,7 +233,7 @@ # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) reg.mref = np.zeros(nC) @@ -345,7 +345,7 @@ data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization -reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) +reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] reg.mref = np.zeros(nC) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index e4004a0b29..aba2d0fde0 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -196,7 +196,7 @@ w = w / np.max(w) w = w -reg = regularization.Smallness(mesh=mesh, indActive=actCells, cell_weights=w) +reg = regularization.Smallness(mesh=mesh, active_cells=actCells, cell_weights=w) opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 ) diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index 1caac9b8d2..465390fca9 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -146,7 +146,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) opt = optimization.InexactGaussNewton(maxIter=15) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) beta = directives.BetaSchedule(coolingFactor=5, coolingRate=2) diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index 188662e72e..b6c67f0fb3 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -155,7 +155,7 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): # Related to inversion reg = regularization.Sparse( - mesh, indActive=actind, mapping=regmap, gradientType="components" + mesh, active_cells=actind, mapping=regmap, gradientType="components" ) # gradientType = 'components' reg.norms = [p, qx, qz, 0.0] diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index 26b9cbf680..c11bea15e9 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -102,7 +102,7 @@ def run(plotIt=True): rxLoc = survey.source_field.receiver_list[0].locations # Create a regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] # Data misfit function diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 2ae4b535e3..661ed94062 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -104,7 +104,7 @@ def run(plotIt=True): data_object = data.Data(survey, dobs=synthetic_data, noise_floor=wd) # Create a regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.mref = np.zeros(nC) reg.norms = [0, 0, 0, 0] # reg.eps_p, reg.eps_q = 1e-0, 1e-0 diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index 4e9fc71442..64a8cbcd0a 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -30,7 +30,7 @@ def setUp(self): corr = regularization.LinearCorrespondence( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh diff --git a/tests/base/test_cross_gradient.py b/tests/base/test_cross_gradient.py index 9c764d481e..b0493b8569 100644 --- a/tests/base/test_cross_gradient.py +++ b/tests/base/test_cross_gradient.py @@ -30,7 +30,7 @@ def setUp(self): cros_grad = regularization.CrossGradient( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -122,7 +122,7 @@ def setUp(self): cros_grad = regularization.CrossGradient( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -196,7 +196,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - cross_grad = regularization.CrossGradient(mesh, wire_map=wires, indActive=actv) + cross_grad = regularization.CrossGradient( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.cross_grad = cross_grad @@ -259,7 +261,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - cross_grad = regularization.CrossGradient(mesh, wire_map=wires, indActive=actv) + cross_grad = regularization.CrossGradient( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.cross_grad = cross_grad diff --git a/tests/base/test_jtv.py b/tests/base/test_jtv.py index 9d93d26c66..c016043da6 100644 --- a/tests/base/test_jtv.py +++ b/tests/base/test_jtv.py @@ -31,7 +31,7 @@ def setUp(self): jtv = regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -81,7 +81,7 @@ def setUp(self): jtv = regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -127,7 +127,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - jtv = regularization.JointTotalVariation(mesh, wire_map=wires, indActive=actv) + jtv = regularization.JointTotalVariation( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.jtv = jtv @@ -174,7 +176,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - jtv = regularization.JointTotalVariation(mesh, wire_map=wires, indActive=actv) + jtv = regularization.JointTotalVariation( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.jtv = jtv @@ -221,7 +225,7 @@ def test_bad_wires(): regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index d7d57f6d80..48279e4b54 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -1,3 +1,4 @@ +import pytest import unittest import numpy as np @@ -22,7 +23,9 @@ def test_depth_weighting_3D(self): r_loc = 0.1 # Depth weighting - wz = utils.depth_weighting(mesh, r_loc, indActive=actv, exponent=5, threshold=0) + wz = utils.depth_weighting( + mesh, r_loc, active_cells=actv, exponent=5, threshold=0 + ) reference_locs = ( np.random.rand(1000, 3) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) @@ -31,14 +34,14 @@ def test_depth_weighting_3D(self): reference_locs[:, -1] = r_loc wz2 = utils.depth_weighting( - mesh, reference_locs, indActive=actv, exponent=5, threshold=0 + mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 ) np.testing.assert_allclose(wz, wz2) # testing default params all_active = np.ones(mesh.n_cells, dtype=bool) wz = utils.depth_weighting( - mesh, r_loc, indActive=all_active, exponent=2, threshold=0.5 * dh + mesh, r_loc, active_cells=all_active, exponent=2, threshold=0.5 * dh ) wz2 = utils.depth_weighting(mesh, r_loc) @@ -58,7 +61,9 @@ def test_depth_weighting_2D(self): r_loc = 0.1 # Depth weighting - wz = utils.depth_weighting(mesh, r_loc, indActive=actv, exponent=5, threshold=0) + wz = utils.depth_weighting( + mesh, r_loc, active_cells=actv, exponent=5, threshold=0 + ) reference_locs = ( np.random.rand(1000, 2) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) @@ -67,10 +72,30 @@ def test_depth_weighting_2D(self): reference_locs[:, -1] = r_loc wz2 = utils.depth_weighting( - mesh, reference_locs, indActive=actv, exponent=5, threshold=0 + mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 ) np.testing.assert_allclose(wz, wz2) +@pytest.fixture +def mesh(): + """Sample mesh.""" + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hz], "CN") + return mesh + + +def test_removed_indactive(mesh): + """ + Test if error is raised after passing removed indActive argument + """ + active_cells = np.ones(mesh.nC, dtype=bool) + msg = "'indActive' argument has been removed. " "Please use 'active_cells' instead." + with pytest.raises(TypeError, match=msg): + utils.depth_weighting(mesh, 0, indActive=active_cells) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 18aac616b6..3000d23460 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -764,7 +764,6 @@ class TestDeprecatedArguments: Within these arguments are: - * ``indActive`` (replaced by ``active_cells``) * ``cell_weights`` (replaced by ``weights``) """ @@ -783,18 +782,6 @@ def mesh(self, request): h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] return discretize.TensorMesh(h) - @pytest.mark.parametrize( - "regularization_class", (BaseRegularization, WeightedLeastSquares) - ) - def test_active_cells(self, mesh, regularization_class): - """Test indActive and active_cells arguments.""" - active_cells = np.ones(len(mesh), dtype=bool) - msg = "Cannot simultaneously pass 'active_cells' and 'indActive'." - with pytest.raises(ValueError, match=msg): - regularization_class( - mesh, active_cells=active_cells, indActive=active_cells - ) - def test_weights(self, mesh): """Test cell_weights and weights.""" weights = np.ones(len(mesh)) @@ -803,6 +790,56 @@ def test_weights(self, mesh): BaseRegularization(mesh, weights=weights, cell_weights=weights) +class TestRemovedObjects: + """ + Test if errors are raised after passing removed arguments or trying to + access removed properties. + + * ``indActive`` (replaced by ``active_cells``) + + """ + + @pytest.fixture(params=["1D", "2D", "3D"]) + def mesh(self, request): + """Sample mesh.""" + if request.param == "1D": + hx = np.random.rand(10) + h = [hx / hx.sum()] + elif request.param == "2D": + hx, hy = np.random.rand(10), np.random.rand(9) + h = [h_i / h_i.sum() for h_i in (hx, hy)] + elif request.param == "3D": + hx, hy, hz = np.random.rand(10), np.random.rand(9), np.random.rand(8) + h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] + return discretize.TensorMesh(h) + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + def test_ind_active(self, mesh, regularization_class): + """Test if error is raised when passing the indActive argument.""" + active_cells = np.ones(len(mesh), dtype=bool) + msg = ( + "'indActive' argument has been removed. " + "Please use 'active_cells' instead." + ) + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, indActive=active_cells) + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + def test_ind_active_property(self, mesh, regularization_class): + """Test if error is raised when trying to access the indActive property.""" + active_cells = np.ones(len(mesh), dtype=bool) + reg = regularization_class(mesh, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells." + with pytest.raises(NotImplementedError, match=msg): + reg.indActive + + class TestRemovedRegularizations: """ Test if errors are raised after creating removed regularization classes. diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 8f9690f216..3c7305f552 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -117,13 +117,13 @@ def setUp(self): # Create three regularization for the different components # of magnetization - reg_p = regularization.Sparse(mesh, indActive=actv, mapping=wires.p) + reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.p) reg_p.mref = np.zeros(3 * nC) - reg_s = regularization.Sparse(mesh, indActive=actv, mapping=wires.s) + reg_s = regularization.Sparse(mesh, active_cells=actv, mapping=wires.s) reg_s.mref = np.zeros(3 * nC) - reg_t = regularization.Sparse(mesh, indActive=actv, mapping=wires.t) + reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.t) reg_t.mref = np.zeros(3 * nC) reg = reg_p + reg_s + reg_t @@ -171,18 +171,18 @@ def setUp(self): # Create a Combo Regularization # Regularize the amplitude of the vectors - reg_a = regularization.Sparse(mesh, indActive=actv, mapping=wires.amp) + reg_a = regularization.Sparse(mesh, active_cells=actv, mapping=wires.amp) reg_a.norms = [0.0, 0.0, 0.0, 0.0] # Sparse on the model and its gradients reg_a.mref = np.zeros(3 * nC) # Regularize the vertical angle of the vectors - reg_t = regularization.Sparse(mesh, indActive=actv, mapping=wires.theta) + reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.theta) reg_t.alpha_s = 0.0 # No reference angle reg_t.space = "spherical" reg_t.norms = [2.0, 0.0, 0.0, 0.0] # Only norm on gradients used # Regularize the horizontal angle of the vectors - reg_p = regularization.Sparse(mesh, indActive=actv, mapping=wires.phi) + reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.phi) reg_p.alpha_s = 0.0 # No reference angle reg_p.space = "spherical" reg_p.norms = [2.0, 0.0, 0.0, 0.0] # Only norm on gradients used diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 5b0dfdf6f6..1f9109d402 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -142,7 +142,7 @@ def setUp(self): # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) reg.mref = np.zeros(nC) @@ -235,7 +235,7 @@ def setUp(self): data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] reg.mref = np.zeros(nC) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index a299c339f5..64fabc6851 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -265,13 +265,13 @@ def setUp(self): # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg_eta = regularization.WeightedLeastSquares( - mesh, mapping=wires.eta, indActive=~airind + mesh, mapping=wires.eta, active_cells=~airind ) reg_taui = regularization.WeightedLeastSquares( - mesh, mapping=wires.taui, indActive=~airind + mesh, mapping=wires.taui, active_cells=~airind ) reg_c = regularization.WeightedLeastSquares( - mesh, mapping=wires.c, indActive=~airind + mesh, mapping=wires.c, active_cells=~airind ) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index 00e5370bd6..a55272f50e 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -282,9 +282,9 @@ def setUp(self): dobs = problem.make_synthetic_data(mSynth, add_noise=True) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) - reg_eta = regularization.Sparse(mesh, mapping=wires.eta, indActive=~airind) - reg_taui = regularization.Sparse(mesh, mapping=wires.taui, indActive=~airind) - reg_c = regularization.Sparse(mesh, mapping=wires.c, indActive=~airind) + reg_eta = regularization.Sparse(mesh, mapping=wires.eta, active_cells=~airind) + reg_taui = regularization.Sparse(mesh, mapping=wires.taui, active_cells=~airind) + reg_c = regularization.Sparse(mesh, mapping=wires.c, active_cells=~airind) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index c9e0cbc7c3..2da13bf2f5 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -101,7 +101,7 @@ def setUp(self): ) # Create a regularization - reg = regularization.Sparse(self.mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] reg.gradientType = "components" diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index 186fdc1343..015aa8bfe0 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -141,7 +141,7 @@ def setUp(self): # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) reg.mref = np.zeros(nC) @@ -234,7 +234,7 @@ def setUp(self): data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] reg.mref = np.zeros(nC) diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index e1e99ebcb7..968ef898a2 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -230,7 +230,9 @@ dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) # Define the regularization (model objective function). -reg = regularization.WeightedLeastSquares(mesh, indActive=ind_active, mapping=model_map) +reg = regularization.WeightedLeastSquares( + mesh, active_cells=ind_active, mapping=model_map +) # Define how the optimization problem is solved. Here we will use a projected # Gauss-Newton approach that employs the conjugate gradient solver. diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index 2cf97a0bc8..50ad50e100 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -302,7 +302,7 @@ reg = regularization.Sparse( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, mapping=regmap, gradientType="total", diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 16498c2aad..34633b0e73 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -300,7 +300,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, ) diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index d4d00efa06..ed14b4fcbe 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -310,7 +310,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, alpha_s=0.01, alpha_x=1, @@ -542,7 +542,7 @@ # Define the regularization (model objective function) ip_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, mapping=maps.IdentityMap(nP=nC), alpha_s=0.01, alpha_x=1, diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index 1be280c5d7..39f8a9a9fd 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -348,7 +348,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, ) @@ -608,7 +608,7 @@ # Define the regularization (model objective function) ip_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, mapping=maps.IdentityMap(nP=nC), alpha_s=0.01, alpha_x=1, diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 261af3baf2..abd1170b8e 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -310,15 +310,15 @@ # Define the regularization (model objective function). reg_grav = regularization.WeightedLeastSquares( - mesh, indActive=ind_active, mapping=wires.density + mesh, active_cells=ind_active, mapping=wires.density ) reg_mag = regularization.WeightedLeastSquares( - mesh, indActive=ind_active, mapping=wires.susceptibility + mesh, active_cells=ind_active, mapping=wires.susceptibility ) # Define the coupling term to connect two different physical property models lamda = 2e12 # weight for coupling term -cross_grad = regularization.CrossGradient(mesh, wires, indActive=ind_active) +cross_grad = regularization.CrossGradient(mesh, wires, active_cells=ind_active) # combo dmis = dmis_grav + dmis_mag diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index 5ac26bff26..6282d22b7f 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -313,7 +313,7 @@ # Define the regularization (model objective function) reg = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_model, alpha_s=1e-2, alpha_x=1, From f2e29ad9580d7ec457ca2f9a9c42b7642656f954 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 13 Mar 2024 09:20:00 -0700 Subject: [PATCH 365/455] Remove the debug argument from InversionDirective (#1370) Remove the `debug` argument and property from `InversionDirective`, add new test to check if it raises an error when passing it as argument. Update directive class using the `debug` property. Part of the solution for https://github.com/simpeg/simpeg/issues/1302 Most of these changes were cherry-picked from https://github.com/simpeg/simpeg/pull/1306 --- SimPEG/directives/directives.py | 11 +++++------ SimPEG/directives/sim_directives.py | 2 +- tests/base/test_directives.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 23addd7899..1f0f3e41e6 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -64,14 +64,13 @@ class InversionDirective: _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): + # Raise error on deprecated arguments + if (key := "debug") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed. Please use 'verbose'.") self.inversion = inversion self.dmisfit = dmisfit self.reg = reg - debug = kwargs.pop("debug", None) - if debug is not None: - self.debug = debug - else: - self.verbose = verbose + self.verbose = verbose set_kwargs(self, **kwargs) @property @@ -89,7 +88,7 @@ def verbose(self, value): self._verbose = validate_type("verbose", value, bool) debug = deprecate_property( - verbose, "debug", "verbose", removal_version="0.19.0", future_warn=True + verbose, "debug", "verbose", removal_version="0.19.0", error=True ) @property diff --git a/SimPEG/directives/sim_directives.py b/SimPEG/directives/sim_directives.py index 718fac26c3..0a3464717d 100644 --- a/SimPEG/directives/sim_directives.py +++ b/SimPEG/directives/sim_directives.py @@ -248,7 +248,7 @@ def initialize(self): if self.seed is not None: np.random.seed(self.seed) - if self.debug: + if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index e4857d08f0..0d2f2f105c 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -294,6 +294,20 @@ def test_save_output_dict(RegClass): assert "x SparseSmoothness.norm" in out_dict +class TestDeprecatedArguments: + """ + Test if directives raise errors after passing deprecated arguments. + """ + + def test_debug(self): + """ + Test if InversionDirective raises error after passing 'debug'. + """ + msg = "'debug' property has been removed. Please use 'verbose'." + with pytest.raises(TypeError, match=msg): + directives.InversionDirective(debug=True) + + class TestUpdateSensitivityWeightsRemovedArgs: """ Test if `UpdateSensitivityWeights` raises errors after passing removed arguments. From 01bc7fa94e0f44316cdf134077123fbac904e3d5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 13 Mar 2024 11:19:44 -0700 Subject: [PATCH 366/455] Remove cellDiff properties of RegularizationMesh (#1371) Remove the `cellDiffx`, `cellDiffy`, and `cellDiffz` properties of `RegularizationMesh`. Part of the solution for #1302 Most of these changes were cherry-picked from #1306 --- SimPEG/regularization/regularization_mesh.py | 9 +++------ tests/base/test_regularization.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 0300adfc44..713ed8b630 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -521,24 +521,21 @@ def cell_gradient_z(self) -> sp.csr_matrix: "cellDiffx", "cell_gradient_x", "0.19.0", - error=False, - future_warn=True, + error=True, ) cellDiffy = deprecate_property( cell_gradient_y, "cellDiffy", "cell_gradient_y", "0.19.0", - error=False, - future_warn=True, + error=True, ) cellDiffz = deprecate_property( cell_gradient_z, "cellDiffz", "cell_gradient_z", "0.19.0", - error=False, - future_warn=True, + error=True, ) @property diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 3000d23460..207e8d9cc8 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -396,14 +396,14 @@ def test_linked_properties(self): ] [self.assertTrue(reg.mapping is fct.mapping) for fct in reg.objfcts] - D = reg.regularization_mesh.cellDiffx + D = reg.regularization_mesh.cell_gradient_x reg.regularization_mesh._cell_gradient_x = 4 * D v = np.random.rand(D.shape[1]) [ self.assertTrue( np.all( reg.regularization_mesh._cell_gradient_x * v - == fct.regularization_mesh.cellDiffx * v + == fct.regularization_mesh.cell_gradient_x * v ) ) for fct in reg.objfcts From c9acbd4563ed991564610177e19faa9e5c855a61 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 13 Mar 2024 14:44:11 -0700 Subject: [PATCH 367/455] Remove deprecated bits of code (#1372) Remove some additional deprecated bits of code throughout SimPEG's codebase. Part of the solution for #1302 Most of these changes were cherry-picked from #1306 --- SimPEG/directives/directives.py | 4 +- .../frequency_domain/receivers.py | 12 +- .../frequency_domain/sources.py | 15 +- .../natural_source/receivers.py | 22 +-- .../static/resistivity/IODC.py | 12 +- .../static/resistivity/survey.py | 10 +- .../electromagnetics/static/utils/__init__.py | 12 -- .../static/utils/static_utils.py | 168 ------------------ .../electromagnetics/time_domain/receivers.py | 14 +- .../electromagnetics/time_domain/sources.py | 80 +-------- SimPEG/potential_fields/base.py | 8 - SimPEG/utils/__init__.py | 2 +- SimPEG/utils/code_utils.py | 22 +-- SimPEG/utils/coord_utils.py | 4 +- SimPEG/utils/curv_utils.py | 8 +- SimPEG/utils/io_utils/__init__.py | 8 - SimPEG/utils/io_utils/io_utils_pf.py | 32 ---- SimPEG/utils/mat_utils.py | 16 +- SimPEG/utils/mesh_utils.py | 6 +- tests/base/test_utils.py | 6 +- tests/em/fdem/forward/test_FDEM_sources.py | 27 +-- tests/em/fdem/forward/test_properties.py | 16 +- tests/em/tdem/test_TDEM_sources.py | 24 +-- tests/em/tdem/test_properties.py | 10 +- 24 files changed, 97 insertions(+), 441 deletions(-) diff --git a/SimPEG/directives/directives.py b/SimPEG/directives/directives.py index 1f0f3e41e6..3907ca646e 100644 --- a/SimPEG/directives/directives.py +++ b/SimPEG/directives/directives.py @@ -22,7 +22,7 @@ mkvc, set_kwargs, sdiag, - diagEst, + estimate_diagonal, spherical2cartesian, cartesian2spherical, Zero, @@ -2418,7 +2418,7 @@ def JtJv(v): return self.simulation.Jtvec(m, Jv) - JtJdiag = diagEst(JtJv, len(m), k=self.k) + JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) JtJdiag = JtJdiag / max(JtJdiag) self.reg.wght = JtJdiag diff --git a/SimPEG/electromagnetics/frequency_domain/receivers.py b/SimPEG/electromagnetics/frequency_domain/receivers.py index b6423da9a0..a28c840ff3 100644 --- a/SimPEG/electromagnetics/frequency_domain/receivers.py +++ b/SimPEG/electromagnetics/frequency_domain/receivers.py @@ -1,6 +1,5 @@ from ... import survey from ...utils import validate_string, validate_type, validate_direction -import warnings from discretize.utils import Zero @@ -33,15 +32,8 @@ def __init__( use_source_receiver_offset=False, **kwargs, ): - proj = kwargs.pop("projComp", None) - if proj is not None: - warnings.warn( - "'projComp' overrides the 'orientation' property which automatically" - " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0.", - stacklevel=2, - ) - self.projComp = proj + if (key := "projComp") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed.") self.orientation = orientation self.component = component diff --git a/SimPEG/electromagnetics/frequency_domain/sources.py b/SimPEG/electromagnetics/frequency_domain/sources.py index bc54e0fdf4..1c739df1b7 100644 --- a/SimPEG/electromagnetics/frequency_domain/sources.py +++ b/SimPEG/electromagnetics/frequency_domain/sources.py @@ -770,11 +770,12 @@ def __init__( **kwargs, ): kwargs.pop("moment", None) - N = kwargs.pop("N", None) - if N is not None: - self.N = N - else: - self.n_turns = n_turns + + # Raise error on deprecated arguments + if (key := "N") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") + self.n_turns = n_turns + super().__init__( receiver_list=receiver_list, frequency=frequency, @@ -871,7 +872,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): ) return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - N = deprecate_property(n_turns, "N", "n_turns", removal_version="0.19.0") + N = deprecate_property( + n_turns, "N", "n_turns", removal_version="0.19.0", error=True + ) class PrimSecSigma(BaseFDEMSrc): diff --git a/SimPEG/electromagnetics/natural_source/receivers.py b/SimPEG/electromagnetics/natural_source/receivers.py index e7c9845630..f0b74fdca4 100644 --- a/SimPEG/electromagnetics/natural_source/receivers.py +++ b/SimPEG/electromagnetics/natural_source/receivers.py @@ -1,4 +1,4 @@ -from ...utils.code_utils import deprecate_class, validate_string +from ...utils.code_utils import validate_string import numpy as np from scipy.constants import mu_0 @@ -613,23 +613,3 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): if adjoint: return imp_deriv return getattr(imp_deriv, self.component) - - -############ -# Deprecated -############ - - -@deprecate_class(removal_version="0.19.0", error=True) -class Point_impedance1D(PointNaturalSource): - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class Point_impedance3D(PointNaturalSource): - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class Point_tipper3D(Point3DTipper): - pass diff --git a/SimPEG/electromagnetics/static/resistivity/IODC.py b/SimPEG/electromagnetics/static/resistivity/IODC.py index 2fc42de988..cc47403d7e 100644 --- a/SimPEG/electromagnetics/static/resistivity/IODC.py +++ b/SimPEG/electromagnetics/static/resistivity/IODC.py @@ -10,7 +10,7 @@ from ....utils import ( sdiag, - uniqueRows, + unique_rows, plot2Ddata, validate_type, validate_integer, @@ -728,12 +728,6 @@ def geometric_factor(self, survey): G = geometric_factor(survey, space_type=self.space_type) return G - def from_ambn_locations_to_survey(self, *args, **kwargs): - raise NotImplementedError( - "from_ambn_locations_to_survey has been renamed to " - "from_abmn_locations_to_survey. It will be removed in a future version 0.17.0 of simpeg", - ) - def from_abmn_locations_to_survey( self, a_locations, @@ -767,8 +761,8 @@ def from_abmn_locations_to_survey( if times_ip is not None: self.times_ip = times_ip - uniqSrc = uniqueRows(np.c_[self.a_locations, self.b_locations]) - uniqElec = uniqueRows( + uniqSrc = unique_rows(np.c_[self.a_locations, self.b_locations]) + uniqElec = unique_rows( np.vstack( (self.a_locations, self.b_locations, self.m_locations, self.n_locations) ) diff --git a/SimPEG/electromagnetics/static/resistivity/survey.py b/SimPEG/electromagnetics/static/resistivity/survey.py index ecbbe58c37..cb02a32e65 100644 --- a/SimPEG/electromagnetics/static/resistivity/survey.py +++ b/SimPEG/electromagnetics/static/resistivity/survey.py @@ -1,6 +1,6 @@ import numpy as np -from ....utils.code_utils import deprecate_property, validate_string +from ....utils.code_utils import validate_string from ....survey import BaseSurvey from ..utils import drapeTopotoLoc @@ -152,14 +152,6 @@ def unique_electrode_locations(self): 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.17.0", - error=True, - ) - @property def source_locations(self): """ diff --git a/SimPEG/electromagnetics/static/utils/__init__.py b/SimPEG/electromagnetics/static/utils/__init__.py index 1e5ac6adbb..a0d69fe512 100644 --- a/SimPEG/electromagnetics/static/utils/__init__.py +++ b/SimPEG/electromagnetics/static/utils/__init__.py @@ -48,34 +48,22 @@ """ from .static_utils import ( electrode_separations, - source_receiver_midpoints, pseudo_locations, geometric_factor, apparent_resistivity_from_voltage, - apparent_resistivity, plot_pseudosection, generate_dcip_survey, - generate_dcip_survey_line, - gen_DCIPsurvey, generate_dcip_sources_line, generate_survey_from_abmn_locations, - writeUBC_DCobs, - writeUBC_DClocs, convert_survey_3d_to_2d_lines, - convertObs_DC3D_to_2D, - readUBC_DC2Dpre, - readUBC_DC3Dobs, xy_2_lineID, r_unit, - getSrc_locs, gettopoCC, drapeTopotoLoc, genTopography, closestPointsGrid, gen_3d_survey_from_2d_lines, plot_1d_layer_model, - plot_layer, - plot_pseudoSection, ) # Import if user has plotly diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/SimPEG/electromagnetics/static/utils/static_utils.py index c52aaf81d2..46d43aa0a5 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/SimPEG/electromagnetics/static/utils/static_utils.py @@ -23,7 +23,6 @@ from ....utils.plot_utils import plot_1d_layer_model # noqa: F401 -from ....utils.code_utils import deprecate_method try: import plotly.graph_objects as grapho @@ -1816,170 +1815,3 @@ def gen_3d_survey_from_2d_lines( line_inds=line_inds, ) return IO_3d, survey_3d - - -############ -# Deprecated -############ - - -def plot_pseudoSection( - data, - ax=None, - survey_type="dipole-dipole", - data_type="appConductivity", - space_type="half-space", - clim=None, - scale="linear", - sameratio=True, - pcolor_opts=None, - data_location=False, - dobs=None, - dim=2, -): - raise TypeError( - "The plot_pseudoSection method has been removed. Please use " - "plot_pseudosection instead." - ) - - -def apparent_resistivity( - data_object, - survey_type=None, - space_type="half space", - dobs=None, - eps=1e-10, - **kwargs, -): - raise TypeError( - "The apparent_resistivity method has been removed. Please use " - "apparent_resistivity_from_voltage instead." - ) - - -source_receiver_midpoints = deprecate_method( - pseudo_locations, "source_receiver_midpoints", "0.17.0", error=True -) - - -def plot_layer(rho, mesh, **kwargs): - raise NotImplementedError( - "The plot_layer method has been deprecated. Please use " - "plot_1d_layer_model instead. This will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def convertObs_DC3D_to_2D(survey, lineID, flag="local"): - raise TypeError( - "The convertObs_DC3D_to_2D method has been removed. Please use " - "convert_3d_survey_to_2d." - ) - - -def getSrc_locs(survey): - raise NotImplementedError( - "The getSrc_locs method has been deprecated. Source " - "locations are now computed as a method of the survey " - "class. Please use Survey.source_locations(). This method " - " will be removed in version 0.17.0 of SimPEG", - ) - - -def writeUBC_DCobs( - fileName, - data, - dim, - format_type, - survey_type="dipole-dipole", - ip_type=0, - comment_lines="", -): - # """ - # Write UBC GIF DCIP 2D or 3D observation file - - # Input: - # :param str fileName: including path where the file is written out - # :param SimPEG.Data data: DC data object - # :param int dim: either 2 | 3 - # :param str format_type: either 'surface' | 'general' | 'simple' - # :param str survey_type: 'dipole-dipole' | 'pole-dipole' | - # 'dipole-pole' | 'pole-pole' | 'gradient' - - # Output: - # :return: UBC2D-Data file - # :rtype: file - # """ - - raise NotImplementedError( - "The writeUBC_DCobs method has been deprecated. Please use " - "write_dcip2d_ubc or write_dcip3d_ubc instead. These are imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def writeUBC_DClocs( - fileName, - dc_survey, - dim, - format_type, - survey_type="dipole-dipole", - ip_type=0, - comment_lines="", -): - # """ - # Write UBC GIF DCIP 2D or 3D locations file - - # Input: - # :param str fileName: including path where the file is written out - # :param SimPEG.electromagnetics.static.resistivity.Survey dc_survey: DC survey object - # :param int dim: either 2 | 3 - # :param str survey_type: either 'SURFACE' | 'GENERAL' - - # Output: - # :rtype: file - # :return: UBC 2/3D-locations file - # """ - - raise NotImplementedError( - "The writeUBC_DClocs method has been deprecated. Please use " - "write_dcip2d_ubc or write_dcip3d_ubc instead. These are imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - FutureWarning, - ) - - -def readUBC_DC2Dpre(fileName): - raise NotImplementedError( - "The readUBC_DC2Dpre method has been deprecated. Please use " - "read_dcip2d_ubc instead. This is imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def readUBC_DC3Dobs(fileName, data_type="volt"): - raise NotImplementedError( - "The readUBC_DC3Dobs method has been deprecated. Please use " - "read_dcip3d_ubc instead. This is imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -gen_DCIPsurvey = deprecate_method( - generate_dcip_survey, "gen_DCIPsurvey", removal_version="0.17.0", error=True -) - - -def generate_dcip_survey_line( - survey_type, data_type, endl, topo, ds, dh, n, dim_flag="2.5D", sources_only=False -): - raise NotImplementedError( - "The gen_dcip_survey_line method has been deprecated. Please use " - "generate_dcip_sources_line instead. This will be removed in version" - " 0.17.0 of SimPEG", - FutureWarning, - ) diff --git a/SimPEG/electromagnetics/time_domain/receivers.py b/SimPEG/electromagnetics/time_domain/receivers.py index 3179c527af..9d6e46ccbb 100644 --- a/SimPEG/electromagnetics/time_domain/receivers.py +++ b/SimPEG/electromagnetics/time_domain/receivers.py @@ -3,7 +3,6 @@ from ...utils import mkvc, validate_type, validate_direction from discretize.utils import Zero from ...survey import BaseTimeRx -import warnings class BaseRx(BaseTimeRx): @@ -25,17 +24,10 @@ def __init__( times, orientation="z", use_source_receiver_offset=False, - **kwargs + **kwargs, ): - proj = kwargs.pop("projComp", None) - if proj is not None: - warnings.warn( - "'projComp' overrides the 'orientation' property which automatically" - " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0.", - stacklevel=2, - ) - self.projComp = proj + if (key := "projComp") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed.") if locations is None: raise AttributeError("'locations' are required. Cannot be 'None'") diff --git a/SimPEG/electromagnetics/time_domain/sources.py b/SimPEG/electromagnetics/time_domain/sources.py index fa37081259..a3c0a64eb9 100644 --- a/SimPEG/electromagnetics/time_domain/sources.py +++ b/SimPEG/electromagnetics/time_domain/sources.py @@ -139,33 +139,6 @@ def eval_deriv(self, time): """ raise NotImplementedError # needed for E-formulation - ########################## - # Deprecated - ########################## - hasInitialFields = deprecate_property( - has_initial_fields, - "hasInitialFields", - new_name="has_initial_fields", - removal_version="0.17.0", - error=True, - ) - - offTime = deprecate_property( - off_time, - "offTime", - new_name="off_time", - removal_version="0.17.0", - error=True, - ) - - eps = deprecate_property( - epsilon, - "eps", - new_name="epsilon", - removal_version="0.17.0", - error=True, - ) - class StepOffWaveform(BaseWaveform): """ @@ -316,14 +289,6 @@ def waveform_function(self, value): def eval(self, time): # noqa: A003 return self.waveform_function(time) - waveFct = deprecate_property( - waveform_function, - "waveFct", - new_name="waveform_function", - removal_version="0.17.0", - error=True, - ) - class VTEMWaveform(BaseWaveform): """ @@ -428,26 +393,6 @@ def eval_deriv(self, time): def time_nodes(self): return np.r_[0, self.peak_time, self.off_time] - ########################## - # Deprecated - ########################## - - peakTime = deprecate_property( - peak_time, - "peakTime", - new_name="peak_time", - removal_version="0.17.0", - error=True, - ) - - a = deprecate_property( - ramp_on_rate, - "a", - new_name="ramp_on_rate", - removal_version="0.17.0", - error=True, - ) - class TrapezoidWaveform(BaseWaveform): """ @@ -625,18 +570,6 @@ def peak_time(self, value): self._ramp_on = np.r_[self._ramp_on[0], value] self._ramp_off = np.r_[value, self._ramp_off[1]] - ########################## - # Deprecated - ########################## - - peakTime = deprecate_property( - peak_time, - "peakTime", - new_name="peak_time", - removal_version="0.17.0", - error=True, - ) - class QuarterSineRampOnWaveform(TrapezoidWaveform): """ @@ -1565,11 +1498,10 @@ def __init__( if "moment" in kwargs: kwargs.pop("moment") - N = kwargs.pop("N", None) - if N is not None: - self.N = N - else: - self.n_turns = n_turns + # Raise error on deprecated arguments + if (key := "N") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") + self.n_turns = n_turns BaseTDEMSrc.__init__( self, receiver_list=receiver_list, location=location, moment=None, **kwargs @@ -1667,7 +1599,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): ) return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - N = deprecate_property(n_turns, "N", "n_turns", removal_version="0.19.0") + N = deprecate_property( + n_turns, "N", "n_turns", removal_version="0.19.0", error=True + ) class LineCurrent(BaseTDEMSrc): diff --git a/SimPEG/potential_fields/base.py b/SimPEG/potential_fields/base.py index 753a80f8fd..9c93895039 100644 --- a/SimPEG/potential_fields/base.py +++ b/SimPEG/potential_fields/base.py @@ -199,14 +199,6 @@ def ind_active(self): """ return self._ind_active - @property - def actInd(self): - """'actInd' is deprecated. Use 'ind_active' instead.""" - raise AttributeError( - "The 'actInd' property has been deprecated. " - "Please use 'ind_active'. This will be removed in version 0.17.0 of SimPEG.", - ) - def linear_operator(self): """Return linear operator. diff --git a/SimPEG/utils/__init__.py b/SimPEG/utils/__init__.py index 49e3e6193a..91d142673d 100644 --- a/SimPEG/utils/__init__.py +++ b/SimPEG/utils/__init__.py @@ -238,7 +238,7 @@ # Deprecated imports interpmat = deprecate_function( - interpolation_matrix, "interpmat", removal_version="0.19.0", future_warn=True + interpolation_matrix, "interpmat", removal_version="0.19.0", error=True ) from .code_utils import ( diff --git a/SimPEG/utils/code_utils.py b/SimPEG/utils/code_utils.py index 87e4c1bede..58e759118d 100644 --- a/SimPEG/utils/code_utils.py +++ b/SimPEG/utils/code_utils.py @@ -322,7 +322,7 @@ def call_hooks(match, mainFirst=False): Use the following syntax:: - @callHooks('doEndIteration') + @call_hooks('doEndIteration') def doEndIteration(self): pass @@ -1233,32 +1233,32 @@ def validate_active_indices(property_name, index_arr, n_cells): # DEPRECATIONS ############################################################### memProfileWrapper = deprecate_function( - mem_profile_class, "memProfileWrapper", removal_version="0.18.0", future_warn=True + mem_profile_class, "memProfileWrapper", removal_version="0.18.0", error=True ) setKwargs = deprecate_function( - set_kwargs, "setKwargs", removal_version="0.18.0", future_warn=True + set_kwargs, "setKwargs", removal_version="0.18.0", error=True ) printTitles = deprecate_function( - print_titles, "printTitles", removal_version="0.18.0", future_warn=True + print_titles, "printTitles", removal_version="0.18.0", error=True ) printLine = deprecate_function( - print_line, "printLine", removal_version="0.18.0", future_warn=True + print_line, "printLine", removal_version="0.18.0", error=True ) printStoppers = deprecate_function( - print_stoppers, "printStoppers", removal_version="0.18.0", future_warn=True + print_stoppers, "printStoppers", removal_version="0.18.0", error=True ) checkStoppers = deprecate_function( - check_stoppers, "checkStoppers", removal_version="0.18.0", future_warn=True + check_stoppers, "checkStoppers", removal_version="0.18.0", error=True ) printDone = deprecate_function( - print_done, "printDone", removal_version="0.18.0", future_warn=True + print_done, "printDone", removal_version="0.18.0", error=True ) callHooks = deprecate_function( - call_hooks, "callHooks", removal_version="0.18.0", future_warn=True + call_hooks, "callHooks", removal_version="0.18.0", error=True ) dependentProperty = deprecate_function( - dependent_property, "dependentProperty", removal_version="0.18.0", future_warn=True + dependent_property, "dependentProperty", removal_version="0.18.0", error=True ) asArray_N_x_Dim = deprecate_function( - as_array_n_by_dim, "asArray_N_x_Dim", removal_version="0.19.0", future_warn=True + as_array_n_by_dim, "asArray_N_x_Dim", removal_version="0.19.0", error=True ) diff --git a/SimPEG/utils/coord_utils.py b/SimPEG/utils/coord_utils.py index bb46021ba9..e1d17c5dbf 100644 --- a/SimPEG/utils/coord_utils.py +++ b/SimPEG/utils/coord_utils.py @@ -9,11 +9,11 @@ rotation_matrix_from_normals, "rotationMatrixFromNormals", removal_version="0.19.0", - future_warn=True, + error=True, ) rotatePointsFromNormals = deprecate_function( rotate_points_from_normals, "rotatePointsFromNormals", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/curv_utils.py b/SimPEG/utils/curv_utils.py index 6f516db1c9..71e764ce60 100644 --- a/SimPEG/utils/curv_utils.py +++ b/SimPEG/utils/curv_utils.py @@ -8,17 +8,17 @@ # deprecated functions volTetra = deprecate_function( - volume_tetrahedron, "volTetra", removal_version="0.19.0", future_warn=True + volume_tetrahedron, "volTetra", removal_version="0.19.0", error=True ) indexCube = deprecate_function( - index_cube, "indexCube", removal_version="0.19.0", future_warn=True + index_cube, "indexCube", removal_version="0.19.0", error=True ) faceInfo = deprecate_function( - face_info, "faceInfo", removal_version="0.19.0", future_warn=True + face_info, "faceInfo", removal_version="0.19.0", error=True ) exampleLrmGrid = deprecate_function( example_curvilinear_grid, "exampleLrmGrid", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/io_utils/__init__.py b/SimPEG/utils/io_utils/__init__.py index b3226b2c2e..14d628ab3d 100644 --- a/SimPEG/utils/io_utils/__init__.py +++ b/SimPEG/utils/io_utils/__init__.py @@ -18,11 +18,3 @@ write_dcipoctree_ubc, write_dcip_xyz, ) - -# Deprecated -from .io_utils_pf import ( - readUBCmagneticsObservations, - writeUBCmagneticsObservations, - readUBCgravityObservations, - writeUBCgravityObservations, -) diff --git a/SimPEG/utils/io_utils/io_utils_pf.py b/SimPEG/utils/io_utils/io_utils_pf.py index b5048d908a..9387896481 100644 --- a/SimPEG/utils/io_utils/io_utils_pf.py +++ b/SimPEG/utils/io_utils/io_utils_pf.py @@ -1,6 +1,5 @@ import numpy as np from discretize.utils import mkvc -from ...utils.code_utils import deprecate_method def read_mag3d_ubc(obs_file): @@ -379,34 +378,3 @@ def write_gg3d_ubc(filename, data_object): ) print("Observation file saved to: " + filename) - - -# ====================================================== -# Depricated Methods -# ====================================================== - - -readUBCmagneticsObservations = deprecate_method( - read_mag3d_ubc, - "readUBCmagneticsObservations", - removal_version="0.14.4", - error=True, -) -writeUBCmagneticsObservations = deprecate_method( - write_mag3d_ubc, - "writeUBCmagneticsObservations", - removal_version="0.14.4", - error=True, -) -readUBCgravityObservations = deprecate_method( - read_grav3d_ubc, - "readUBCgravityObservations", - removal_version="0.14.4", - error=True, -) -writeUBCgravityObservations = deprecate_method( - write_grav3d_ubc, - "writeUBCgravityObservations", - removal_version="0.14.4", - error=True, -) diff --git a/SimPEG/utils/mat_utils.py b/SimPEG/utils/mat_utils.py index c7b3d07fcb..46798e75dd 100644 --- a/SimPEG/utils/mat_utils.py +++ b/SimPEG/utils/mat_utils.py @@ -451,36 +451,36 @@ def define_plane_from_points(xyz1, xyz2, xyz3): diagEst = deprecate_function( - estimate_diagonal, "diagEst", removal_version="0.19.0", future_warn=True + estimate_diagonal, "diagEst", removal_version="0.19.0", error=True ) uniqueRows = deprecate_function( - unique_rows, "uniqueRows", removal_version="0.19.0", future_warn=True + unique_rows, "uniqueRows", removal_version="0.19.0", error=True ) -sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", future_warn=True) +sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", error=True) getSubArray = deprecate_function( - get_subarray, "getSubArray", removal_version="0.19.0", future_warn=True + get_subarray, "getSubArray", removal_version="0.19.0", error=True ) inv3X3BlockDiagonal = deprecate_function( inverse_3x3_block_diagonal, "inv3X3BlockDiagonal", removal_version="0.19.0", - future_warn=True, + error=True, ) inv2X2BlockDiagonal = deprecate_function( inverse_2x2_block_diagonal, "inv2X2BlockDiagonal", removal_version="0.19.0", - future_warn=True, + error=True, ) makePropertyTensor = deprecate_function( make_property_tensor, "makePropertyTensor", removal_version="0.19.0", - future_warn=True, + error=True, ) invPropertyTensor = deprecate_function( inverse_property_tensor, "invPropertyTensor", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/mesh_utils.py b/SimPEG/utils/mesh_utils.py index 30a7e52143..0d39f5c162 100644 --- a/SimPEG/utils/mesh_utils.py +++ b/SimPEG/utils/mesh_utils.py @@ -101,11 +101,11 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): # DEPRECATED FUNCTIONS ################################################ meshTensor = deprecate_function( - unpack_widths, "meshTensor", removal_version="0.19.0", future_warn=True + unpack_widths, "meshTensor", removal_version="0.19.0", error=True ) closestPoints = deprecate_function( - closest_points_index, "closestPoints", removal_version="0.19.0", future_warn=True + closest_points_index, "closestPoints", removal_version="0.19.0", error=True ) ExtractCoreMesh = deprecate_function( - extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", future_warn=True + extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", error=True ) diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index 5952b755fc..a63ed30702 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -17,7 +17,7 @@ ind2sub, as_array_n_by_dim, TensorType, - diagEst, + estimate_diagonal, count, timeIt, Counter, @@ -300,14 +300,14 @@ def test_surface2ind_topo(self): assert len(np.where(indtopoN)[0]) == 8211 -class TestDiagEst(unittest.TestCase): +class TestEstimateDiagonal(unittest.TestCase): def setUp(self): self.n = 1000 self.A = np.random.rand(self.n, self.n) self.Adiag = np.diagonal(self.A) def getTest(self, testType): - Adiagtest = diagEst(self.A, self.n, self.n, testType) + Adiagtest = estimate_diagonal(self.A, self.n, self.n, testType) r = np.abs(Adiagtest - self.Adiag) err = r.dot(r) return err diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index e044e51b28..4b499655c3 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -376,21 +376,22 @@ def test_CircularLoop_bPrimaryMu50_h(self): assert self.bPrimaryTest(src, "j") -def test_CircularLoop_test_N_assign(): +def test_removal_circular_loop_n(): """ - Test depreciation of the N argument (now n_turns) + Test if passing the N argument to CircularLoop raises an error """ - src = fdem.sources.CircularLoop( - [], - frequency=1e-3, - radius=np.sqrt(1 / np.pi), - location=[0, 0, 0], - orientation="Z", - mu=mu_0, - current=0.5, - N=2, - ) - assert src.n_turns == 2 + msg = "'N' property has been removed. Please use 'n_turns'." + with pytest.raises(TypeError, match=msg): + fdem.sources.CircularLoop( + [], + frequency=1e-3, + radius=np.sqrt(1 / np.pi), + location=[0, 0, 0], + orientation="Z", + mu=mu_0, + current=0.5, + N=2, + ) def test_line_current_failures(): diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index aca1cca714..5384c69d17 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -5,18 +5,12 @@ from SimPEG.electromagnetics import time_domain as tdem -def test_receiver_properties_validation(): +def test_removed_projcomp(): + """Test if passing the removed `projComp` argument raises an error.""" xyz = np.c_[0.0, 0.0, 0.0] - projComp = "Fx" - rx = fdem.receivers.BaseRx(xyz, projComp=projComp) - - assert rx.projComp == projComp - - with pytest.raises(ValueError): - fdem.receivers.BaseRx(xyz, component="potato") - - with pytest.raises(TypeError): - fdem.receivers.BaseRx(xyz, component=2.0) + msg = "'projComp' property has been removed." + with pytest.raises(TypeError, match=msg): + fdem.receivers.BaseRx(xyz, projComp="foo") def test_source_properties_validation(): diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 7332c49db3..8d3aa9b511 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -1,3 +1,4 @@ +import pytest import unittest import numpy as np @@ -527,16 +528,17 @@ def test_simple_source(): assert waveform.eval(0.0) == 1.0 -def test_CircularLoop_test_N_assignment(): +def test_removal_circular_loop_n(): """ - Test depreciation of the N property + Test if passing the N argument to CircularLoop raises an error """ - loop = CircularLoop( - [], - waveform=StepOffWaveform(), - location=np.array([0.0, 0.0, 0.0]), - radius=1.0, - current=0.5, - N=2, - ) - assert loop.n_turns == 2 + msg = "'N' property has been removed. Please use 'n_turns'." + with pytest.raises(TypeError, match=msg): + CircularLoop( + [], + waveform=StepOffWaveform(), + location=np.array([0.0, 0.0, 0.0]), + radius=1.0, + current=0.5, + N=2, + ) diff --git a/tests/em/tdem/test_properties.py b/tests/em/tdem/test_properties.py index 5aa443b45b..1c1bb50136 100644 --- a/tests/em/tdem/test_properties.py +++ b/tests/em/tdem/test_properties.py @@ -4,13 +4,13 @@ from SimPEG.electromagnetics import time_domain as tdem -def test_receiver_properties(): +def test_removed_projcomp(): + """Test if passing the removed `projComp` argument raises an error.""" xyz = np.c_[0.0, 0.0, 0.0] times = np.logspace(-5, -2, 4) - projComp = "Fx" - rx = tdem.receivers.BaseRx(xyz, times, projComp=projComp) - - assert rx.projComp == projComp + msg = "'projComp' property has been removed." + with pytest.raises(TypeError, match=msg): + tdem.receivers.BaseRx(xyz, times, projComp="foo") def test_source_properties(): From 4a3bab119989901d64e6013c40a1f03691acf817 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 14 Mar 2024 09:48:39 -0700 Subject: [PATCH 368/455] Use choclo in gravity tutorials (#1378) Use `engine="choclo"` in tutorials that make use of gravity simulations. Add an admonition instructing on how to use the `engine` argument. --- .../03-gravity/plot_1a_gravity_anomaly.py | 20 +++++++++++++++++-- .../03-gravity/plot_1b_gravity_gradiometry.py | 20 +++++++++++++++++-- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 13 +++++++++++- .../plot_inv_1b_gravity_anomaly_irls.py | 13 +++++++++++- .../plot_inv_3_cross_gradient_pf.py | 14 ++++++++++++- ...t_inv_1_joint_pf_pgi_full_info_tutorial.py | 1 + ...lot_inv_2_joint_pf_pgi_no_info_tutorial.py | 1 + 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index f511e9e2a1..22721d3fcb 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -180,19 +180,35 @@ # formulation. # -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities +############################################################################### +# Define the forward simulation. By setting the ``store_sensitivities`` keyword +# argument to ``"forward_only"``, we simulate the data without storing the +# sensitivities. +# + simulation = gravity.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active, store_sensitivities="forward_only", + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.21.0 we can use `Choclo +# `_ as the engine for running the gravity +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + +############################################################################### # Compute predicted data for some model # SimPEG uses right handed coordinate where Z is positive upward. # This causes gravity signals look "inconsistent" with density values in visualization. + dpred = simulation.dpred(model) # Plot diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 06753d7f2d..7ebeea969e 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -201,17 +201,33 @@ # formulation. # -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities +############################################################################### +# Define the forward simulation. By setting the ``store_sensitivities`` keyword +# argument to ``"forward_only"``, we simulate the data without storing the +# sensitivities +# + simulation = gravity.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active, store_sensitivities="forward_only", + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.21.0 we can use `Choclo +# `_ as the engine for running the gravity +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + +############################################################################### # Compute predicted data for some model + dpred = simulation.dpred(model) n_data = len(dpred) diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 968ef898a2..34fd346d83 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -206,9 +206,20 @@ # Here, we define the physics of the gravity problem by using the simulation # class. # +# .. tip:: +# +# Since SimPEG v0.21.0 we can use `Choclo +# `_ as the engine for running the gravity +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active + survey=survey, + mesh=mesh, + rhoMap=model_map, + ind_active=ind_active, + engine="choclo", ) diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 23336dee7c..cc658a6d27 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -208,9 +208,20 @@ # Here, we define the physics of the gravity problem by using the simulation # class. # +# .. tip:: +# +# Since SimPEG v0.21.0 we can use `Choclo +# `_ as the engine for running the gravity +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active + survey=survey, + mesh=mesh, + rhoMap=model_map, + ind_active=ind_active, + engine="choclo", ) diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index abd1170b8e..cf271661d2 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -275,9 +275,21 @@ # Here, we define the physics of the gravity and magnetic problems by using the simulation # class. # +# .. tip:: +# +# Since SimPEG v0.21.0 we can use `Choclo +# `_ as the engine for running the gravity +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + simulation_grav = gravity.simulation.Simulation3DIntegral( - survey=survey_grav, mesh=mesh, rhoMap=wires.density, ind_active=ind_active + survey=survey_grav, + mesh=mesh, + rhoMap=wires.density, + ind_active=ind_active, + engine="choclo", ) simulation_mag = magnetics.simulation.Simulation3DIntegral( diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index 59e842354d..96b83c4b36 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -233,6 +233,7 @@ mesh=mesh, rhoMap=wires.den, ind_active=actv, + engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) # Mag problem diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index 6880a7c15c..ba0219e59a 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -234,6 +234,7 @@ mesh=mesh, rhoMap=wires.den, ind_active=actv, + engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) # Mag problem From 316fd699c25a81726283d13658acb0c67f9979d1 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 14 Mar 2024 11:37:06 -0700 Subject: [PATCH 369/455] Remove surface2ind_topo (#1374) Remove the `surface2ind_topo` function and replace its usage for `dicretize.utils.active_xyz`. Part of the solution to #1302 --- .../static/utils/static_utils.py | 3 +- SimPEG/utils/__init__.py | 3 +- SimPEG/utils/model_utils.py | 41 ------------------- tests/base/test_utils.py | 24 ----------- 4 files changed, 2 insertions(+), 69 deletions(-) diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/SimPEG/electromagnetics/static/utils/static_utils.py index 46d43aa0a5..573199e260 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/SimPEG/electromagnetics/static/utils/static_utils.py @@ -10,7 +10,6 @@ from .. import resistivity as dc from ....utils import ( mkvc, - surface2ind_topo, model_builder, define_plane_from_points, ) @@ -1626,7 +1625,7 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs raise ValueError("Unsupported mesh dimension") if ind_active is None: - ind_active = surface2ind_topo(mesh, topo) + ind_active = discretize.utils.active_from_xyz(mesh, topo) if mesh._meshType == "TENSOR": meshtemp, topoCC = gettopoCC(mesh, ind_active, option=option) diff --git a/SimPEG/utils/__init__.py b/SimPEG/utils/__init__.py index 91d142673d..c53f37ec75 100644 --- a/SimPEG/utils/__init__.py +++ b/SimPEG/utils/__init__.py @@ -76,7 +76,6 @@ :toctree: generated/ depth_weighting - surface2ind_topo model_builder.add_block model_builder.create_2_layer_model model_builder.create_block_in_wholespace @@ -225,7 +224,7 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .model_utils import surface2ind_topo, depth_weighting +from .model_utils import depth_weighting from .plot_utils import plot2Ddata, plotLayer, plot_1d_layer_model from .io_utils import download from .pgi_utils import ( diff --git a/SimPEG/utils/model_utils.py b/SimPEG/utils/model_utils.py index 2bdd99b42a..91df15da71 100644 --- a/SimPEG/utils/model_utils.py +++ b/SimPEG/utils/model_utils.py @@ -3,47 +3,6 @@ from scipy.interpolate import griddata from scipy.spatial import cKDTree import scipy.sparse as sp -from discretize.utils import active_from_xyz -import warnings - - -def surface2ind_topo(mesh, topo, gridLoc="CC", method="nearest", fill_value=np.nan): - """Get indices of active cells from topography. - - For a mesh and surface topography, this function returns the indices of cells - lying below the discretized surface topography. - - Parameters - ---------- - mesh : discretize.TensorMesh or discretize.TreeMesh - Mesh on which you want to identify active cells - topo : (n, 3) numpy.ndarray - Topography data as a ``numpyndarray`` with columns [x,y,z]; can use [x,z] for 2D meshes. - Topography data can be unstructured. - gridLoc : str {'CC', 'N'} - If 'CC', all cells whose centers are below the topography are active cells. - If 'N', then cells must lie entirely below the topography in order to be active cells. - method : str {'nearest','linear'} - Interpolation method for approximating topography at cell's horizontal position. - Default is 'nearest'. - fill_value : float - Defines the elevation for cells outside the horizontal extent of the topography data. - Default is :py:class:`numpy.nan`. - - Returns - ------- - (n_active) numpy.ndarray of int - Indices of active cells below xyz. - """ - warnings.warn( - "The surface2ind_topo function has been deprecated, please import " - "discretize.utils.active_from_xyz. This will be removed in SimPEG 0.20.0", - FutureWarning, - stacklevel=2, - ) - - active_cells = active_from_xyz(mesh, topo, gridLoc, method) - return np.arange(mesh.n_cells)[active_cells] def surface_layer_index(mesh, topo, index=0): diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index a63ed30702..88938828ca 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -22,7 +22,6 @@ timeIt, Counter, download, - surface2ind_topo, coterminal, ) import discretize @@ -276,29 +275,6 @@ def test_as_array_n_by_dim(self): self.assertTrue(np.all(true == listArray)) self.assertTrue(true.shape == listArray.shape) - def test_surface2ind_topo(self): - file_url = ( - "https://storage.googleapis.com/simpeg/tests/utils/vancouver_topo.xyz" - ) - file2load = download(file_url) - vancouver_topo = np.loadtxt(file2load) - mesh_topo = discretize.TensorMesh( - [[(500.0, 24)], [(500.0, 20)], [(10.0, 30)]], x0="CCC" - ) - - # To keep consistent with result from deprecated function - vancouver_topo[:, 2] = vancouver_topo[:, 2] + 1e-8 - - indtopoCC = surface2ind_topo( - mesh_topo, vancouver_topo, gridLoc="CC", method="nearest" - ) - indtopoN = surface2ind_topo( - mesh_topo, vancouver_topo, gridLoc="N", method="nearest" - ) - - assert len(np.where(indtopoCC)[0]) == 8728 - assert len(np.where(indtopoN)[0]) == 8211 - class TestEstimateDiagonal(unittest.TestCase): def setUp(self): From d8c90424df8d9d7d738397f723ea5c91bde30784 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 14 Mar 2024 22:16:35 -0600 Subject: [PATCH 370/455] Speed up sphinx documentation building (#1382) #### Summary Stops the left sidebar from being completely expandable for every single item in the documentation. https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/navigation.html#remove-reveal-buttons-for-sidebar-items #### Reference issue No number... but the docs take so long to build now.... #### What does this implement/fix? Faster doc building (excluding the examples and tutorials) #### Additional information (https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/performance.html --- docs/_templates/autosummary/attribute.rst | 7 +++++++ docs/_templates/autosummary/base.rst | 9 +++++++++ docs/_templates/autosummary/method.rst | 7 +++++++ docs/conf.py | 5 ++--- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docs/_templates/autosummary/attribute.rst create mode 100644 docs/_templates/autosummary/base.rst create mode 100644 docs/_templates/autosummary/method.rst diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 0000000000..820f45286e --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst new file mode 100644 index 0000000000..ef8e6277cb --- /dev/null +++ b/docs/_templates/autosummary/base.rst @@ -0,0 +1,9 @@ +{% if objtype == 'property' %} +:orphan: +{% endif %} + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst new file mode 100644 index 0000000000..820f45286e --- /dev/null +++ b/docs/_templates/autosummary/method.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index cea63da1c6..72ee283c7f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,7 @@ autosummary_generate = True numpydoc_attributes_as_param_list = False -# This has to be set to false in order to make the doc build in a -# reasonable amount of time. -numpydoc_show_inherited_class_members = False +numpydoc_show_inherited_class_members = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -274,6 +272,7 @@ def linkcode_resolve(domain, info): }, ], "use_edit_page_button": False, + "collapse_navigation": True, } html_logo = "images/simpeg-logo.png" From 27f382f6d5a7326a2318b006f90a3e60d18702f0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 15 Mar 2024 09:11:47 -0700 Subject: [PATCH 371/455] Add docs/sg_execution_times.rst to .gitignore (#1380) #### Summary Ignore the `docs/sg_execution_times.rst` file generated by Sphinx Gallery when building the docs. It contains the execution times of every example in the gallery. We don't need to version control this file. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f6cf59ad22..d85d053a89 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ docs/content/api/generated/* docs/content/examples/* docs/content/tutorials/* docs/modules/* +docs/sg_execution_times.rst .vscode/* # paths to where data are downloaded From 1cb3cea886bb27d46d483790a9e1b76509ce7946 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 15 Mar 2024 13:07:59 -0700 Subject: [PATCH 372/455] Describe merge process of Pull Requests in docs (#1375) Add a description about how the merge process of Pull Requests will be done, detailing that the Squash and Merge will be used. Fixes #1369 --- .../contributing/pull-requests.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/content/getting_started/contributing/pull-requests.rst b/docs/content/getting_started/contributing/pull-requests.rst index 4475ae1828..75ee05bcdd 100644 --- a/docs/content/getting_started/contributing/pull-requests.rst +++ b/docs/content/getting_started/contributing/pull-requests.rst @@ -33,3 +33,22 @@ pull request into the main branch (feel free to ping one of us on Github). This being said, all SimPEG developers and admins are essentially volunteers providing their time for the benefit of the community. This does mean that it might take some time for us to get your PR. + +Merging a Pull Request +---------------------- + +The ``@simpeg/simpeg-admin`` will merge a Pull Request to the `main` branch +using the `Squash and Merge +`_ +strategy: all commits made to the PR branch will be _squashed_ to a single +commit that will be added to `main`. + +SimPEG admins will ensure that the commit message is descriptive and +comprehensive. Contributors can help by providing a descriptive and +comprehensive PR description of the changes that were applied and the reasons +behind them. This will be greatly appreciated. + +Admins will mention other authors that made significant contributions to +the PR in the commit message, following GitHub's approach for `Creating +co-authored commits +`_. From 7420f93db6cf7f6f8a4dc1ecbd6e91f421374770 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 19 Mar 2024 15:27:11 -0700 Subject: [PATCH 373/455] Simplify private methods in gravity simulation (#1384) Simplify some of the new methods included with the Numba implementation of the gravity simulation: remove the `_get_cell_nodes` method since we can just rely on the `cell_nodes` method of discretize meshes. Update one inline comment. --- SimPEG/potential_fields/gravity/simulation.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index a41ea48112..434c6992dc 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -449,15 +449,6 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix - def _get_cell_nodes(self): - """ - Return indices of nodes for each cell in the mesh. - """ - if not isinstance(self.mesh, (discretize.TreeMesh, discretize.TensorMesh)): - raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") - cell_nodes = self.mesh.cell_nodes - return cell_nodes - def _get_active_nodes(self): """ Return locations of nodes only for active cells @@ -473,7 +464,7 @@ def _get_active_nodes(self): else: raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") # Get original cell_nodes but only for active cells - cell_nodes = self._get_cell_nodes() + cell_nodes = self.mesh.cell_nodes # If all cells in the mesh are active, return nodes and cell_nodes if self.nC == self.mesh.n_cells: return nodes, cell_nodes @@ -484,7 +475,7 @@ def _get_active_nodes(self): unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) # Select only the nodes that belong to the active cells (active nodes) active_nodes = nodes[unique_nodes] - # Reshape indices of active cells for each active cell in the mesh + # Reshape indices of active cell nodes for each active cell in the mesh active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) return active_nodes, active_cell_nodes From 3be7d4068dccc4ed4696ad0e804191bb7020245f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 27 Mar 2024 10:27:23 -0700 Subject: [PATCH 374/455] Update Slack links: point to Mattermost (#1385) Replace the links pointing to Slack for links pointing to Mattermost. Changes have been applied to the documentation, the `README.rst` and the issue templates. --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- .github/ISSUE_TEMPLATE/feature-request.yml | 9 +++++---- README.rst | 14 ++++++++------ docs/conf.py | 11 +++++++---- .../content/getting_started/contributing/index.rst | 3 ++- docs/content/getting_started/installing.rst | 2 +- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f4108d53c7..6180153534 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,5 +3,5 @@ contact_links: url: https://simpeg.discourse.group/ about: "If you have a question on how to use SimPEG, please submit them to our discourse page." - name: Development-related matters - url: http://slack.simpeg.xyz/ - about: "If you would like to discuss SimPEG, any geophysics related problems, or need help from the SimPEG team, get in touch with us on slack." + url: https://mattermost.softwareunderground.org/simpeg + about: "If you would like to discuss SimPEG, any geophysics related problems, or need help from the SimPEG team, get in touch with us on Mattermost." diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index c7db801f1a..5d8d196a5e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -6,10 +6,11 @@ body: - type: markdown attributes: value: > - If you'd like to request a new feature in SimPEG, or suggest changes in the - functionality of certain functions, we recommend getting in touch with the - developers on [slack](https://slack.simpeg.xyz), in addition to opening an - issue or pull request here. + If you'd like to request a new feature in SimPEG, or suggest changes in + the functionality of certain functions, we recommend getting in touch + with the developers on + [Mattermost](https://mattermost.softwareunderground.org/simpeg), in + addition to opening an Issue or Pull Request here. You can also check out our [Contributor Guide](https://docs.simpeg.xyz/content/getting_started/contributing/index.html) if you need more information. diff --git a/README.rst b/README.rst index bb8122935b..7545b24db7 100644 --- a/README.rst +++ b/README.rst @@ -30,8 +30,8 @@ SimPEG .. image:: https://img.shields.io/discourse/users?server=http%3A%2F%2Fsimpeg.discourse.group%2F :target: https://simpeg.discourse.group/ -.. image:: https://img.shields.io/badge/Slack-simpeg-4A154B.svg?logo=slack - :target: http://slack.simpeg.xyz +.. image:: https://img.shields.io/badge/simpeg-purple?logo=mattermost&label=Mattermost + :target: https://mattermost.softwareunderground.org/simpeg .. image:: https://img.shields.io/badge/Youtube%20channel-GeoSci.xyz-FF0000.svg?logo=youtube :target: https://www.youtube.com/channel/UCBrC4M8_S4GXhyHht7FyQqw @@ -109,7 +109,8 @@ Questions If you have a question regarding a specific use of SimPEG, the fastest way to get a response is by posting on our Discourse discussion forum: https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, -you can join our slack group at http://slack.simpeg.xyz. +you can join our Mattermost Team at +https://mattermost.softwareunderground.org/simpeg. Please do not create an issue to ask a question. @@ -121,7 +122,8 @@ for developers to discuss upcoming changes to the code base, and for discussing topics related to geophysics in general. Currently our meetings are held every Wednesday, alternating between a mornings (10:30 am pacific time) and afternoons (3:00 pm pacific time) -on even numbered Wednesdays. Find more info on our `slack `_. +on even numbered Wednesdays. Find more info on our +`Mattermost `_. Links @@ -134,8 +136,8 @@ Forums: https://simpeg.discourse.group/ -Slack (real time chat): -http://slack.simpeg.xyz +Mattermost (real time chat): +https://mattermost.softwareunderground.org/simpeg Documentation: diff --git a/docs/conf.py b/docs/conf.py index 72ee283c7f..25936ff517 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -242,7 +242,10 @@ def linkcode_resolve(domain, info): html_theme_options = { "external_links": [ {"name": "SimPEG", "url": "https://simpeg.xyz"}, - {"name": "Contact", "url": "http://slack.simpeg.xyz"}, + { + "name": "Contact", + "url": "https://mattermost.softwareunderground.org/simpeg", + }, ], "icon_links": [ { @@ -251,9 +254,9 @@ def linkcode_resolve(domain, info): "icon": "fab fa-github", }, { - "name": "Slack", - "url": "http://slack.simpeg.xyz/", - "icon": "fab fa-slack", + "name": "Mattermost", + "url": "https://mattermost.softwareunderground.org/simpeg", + "icon": "fas fa-comment", }, { "name": "Discourse", diff --git a/docs/content/getting_started/contributing/index.rst b/docs/content/getting_started/contributing/index.rst index 66a10fd678..cb26c41c54 100644 --- a/docs/content/getting_started/contributing/index.rst +++ b/docs/content/getting_started/contributing/index.rst @@ -20,7 +20,8 @@ Ask questions If you have a question regarding a specific use of SimPEG, the fastest way to get a response is by posting on our Discourse discussion forum: https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, -you can join our slack group at http://slack.simpeg.xyz. +you can join our Mattermost Team at +https://mattermost.softwareunderground.org/simpeg. Please do not create an issue to ask a question. .. _issues: diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst index 7105415ad6..14adff1570 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -104,7 +104,7 @@ be able to download and run any of the :ref:`examples and tutorials `_ or on -`slack `_. +`Mattermost `_. Useful Links ============ From e8367b000cf81c7eff8fa5a9f9e7685a646e4521 Mon Sep 17 00:00:00 2001 From: John Kuttai Date: Wed, 27 Mar 2024 10:50:02 -0700 Subject: [PATCH 375/455] added getJ for fdem and nsem simulations (#1276) Implements methods to calculate sensitivity and sensitivity weights for fdem and nsem simulations Added methods for the FDEM simulation class are: - `getJ` - `getJtJdiag` Co-authored-by: Joseph Capriotti --- SimPEG/base/pde_simulation.py | 3 + .../frequency_domain/simulation.py | 109 +++++++++++++++++- .../natural_source/receivers.py | 26 ++++- .../nsem/inversion/test_Problem3D_Derivs.py | 53 +++++++++ 4 files changed, 184 insertions(+), 7 deletions(-) diff --git a/SimPEG/base/pde_simulation.py b/SimPEG/base/pde_simulation.py index a213d922ad..bf000b4c5c 100644 --- a/SimPEG/base/pde_simulation.py +++ b/SimPEG/base/pde_simulation.py @@ -16,6 +16,9 @@ def __inner_mat_mul_op(M, u, v=None, adjoint=False): # u has multiple fields if v.ndim == 1: v = v[:, None] + if adjoint and v.shape[1] != u.shape[1] and v.shape[1] > 1: + # make sure v is a good shape + v = v.reshape(u.shape[0], -1, u.shape[1]) else: if v.ndim > 1: u = u[:, None] diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py index 50b138cf9e..fd85d50ddb 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/SimPEG/electromagnetics/frequency_domain/simulation.py @@ -59,7 +59,13 @@ class BaseFDEMSimulation(BaseEMSimulation): # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") def __init__( - self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + self, + mesh, + survey=None, + forward_only=False, + permittivity=None, + storeJ=False, + **kwargs ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only @@ -69,6 +75,7 @@ def __init__( stacklevel=2, ) self.permittivity = permittivity + self.storeJ = storeJ @property def survey(self): @@ -87,6 +94,21 @@ def survey(self, value): if value is not None: value = validate_type("survey", value, Survey, cast=False) self._survey = value + self._survey = value + + @property + def storeJ(self): + """Whether to store the sensitivity matrix + + Returns + ------- + bool + """ + return self._storeJ + + @storeJ.setter + def storeJ(self, value): + self._storeJ = validate_type("storeJ", value, bool) @property def forward_only(self): @@ -240,6 +262,86 @@ def Jtvec(self, m, v, f=None): return mkvc(Jtv) + def getJ(self, m, f=None): + """ + Method to form full J given a model m + + :param numpy.ndarray m: inversion model (nP,) + :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :rtype: numpy.ndarray + :return: J (ndata, nP) + """ + self.model = m + + if getattr(self, "_Jmatrix", None) is None: + if f is None: + f = self.fields(m) + + Ainv = self.Ainv + m_size = self.model.size + + Jmatrix = np.zeros((self.survey.nD, m_size)) + + data = Data(self.survey) + + for A_i, freq in zip(Ainv, self.survey.frequencies): + for src in self.survey.get_sources_by_frequency(freq): + u_src = f[src, self._solutionType] + + for rx in src.receiver_list: + v = np.eye(rx.nD, dtype=float) + + df_duT, df_dmT = rx.evalDeriv( + src, self.mesh, f, v=v, adjoint=True + ) + + df_duT = np.hstack([df_duT]) + ATinvdf_duT = A_i * df_duT + dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) + dRHS_dmT = self.getRHSDeriv( + freq, src, ATinvdf_duT, adjoint=True + ) + du_dmT = -dA_dmT + + if not isinstance(dRHS_dmT, Zero): + du_dmT += dRHS_dmT + if not isinstance(df_dmT[0], Zero): + du_dmT += np.hstack(df_dmT) + + block = np.array(du_dmT, dtype=complex).real.T + data_inds = data.index_dictionary[src][rx] + Jmatrix[data_inds] = block + + self._Jmatrix = Jmatrix + + return self._Jmatrix + + def getJtJdiag(self, m, W=None, f=None): + """ + Return the diagonal of JtJ + + :param numpy.ndarray m: inversion model (nP,) + :param numpy.ndarray W: vector of weights (ndata,) + :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :rtype: numpy.ndarray + :return: JtJ (nP,) + """ + self.model = m + + if getattr(self, "_gtgdiag", None) is None: + J = self.getJ(m, f=f) + + if W is None: + W = np.ones(J.shape[0]) + else: + W = W.diagonal() ** 2 + + diag = np.einsum("i,ij,ij->j", W, J, J) + + self._gtgdiag = diag + + return self._gtgdiag + # @profile def getSourceTerm(self, freq): """ @@ -272,6 +374,11 @@ def getSourceTerm(self, freq): i = ii return s_m, s_e + @property + def deleteTheseOnModelUpdate(self): + toDelete = super().deleteTheseOnModelUpdate + return toDelete + ["_Jmatrix", "_gtgdiag"] + ############################################################################### # E-B Formulation # diff --git a/SimPEG/electromagnetics/natural_source/receivers.py b/SimPEG/electromagnetics/natural_source/receivers.py index f0b74fdca4..65aa483dc3 100644 --- a/SimPEG/electromagnetics/natural_source/receivers.py +++ b/SimPEG/electromagnetics/natural_source/receivers.py @@ -294,18 +294,25 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals # Work backwards! gtop_v = v / bot gbot_v = -imp * v / bot + n_d = self.nD if mesh.dim == 3: - ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v[:, None] - ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v[:, None] - ge_v = np.c_[h[:, 1], -h[:, 0]] * gtop_v[:, None] - gh_v = np.c_[-e[:, 1], e[:, 0]] * gtop_v[:, None] + ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v[..., None] + ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v[..., None] + ge_v = np.c_[h[:, 1], -h[:, 0]] * gtop_v[..., None] + gh_v = np.c_[-e[:, 1], e[:, 0]] * gtop_v[..., None] if self.orientation[1] == "x": ghy_v += gh_v else: ghx_v -= gh_v + if v.ndim == 2: + # collapse into a long list of n_d vectors + ghx_v = ghx_v.reshape((n_d, -1)) + ghy_v = ghy_v.reshape((n_d, -1)) + ge_v = ge_v.reshape((n_d, -1)) + gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v ge_v = Pe.T @ ge_v else: @@ -515,8 +522,9 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): if adjoint: # Work backwards! - gtop_v = (v / bot)[:, None] - gbot_v = (-imp * v / bot)[:, None] + gtop_v = (v / bot)[..., None] + gbot_v = (-imp * v / bot)[..., None] + n_d = self.nD ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v @@ -528,6 +536,12 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): else: ghx_v += gh_v + if v.ndim == 2: + # collapse into a long list of n_d vectors + ghx_v = ghx_v.reshape((n_d, -1)) + ghy_v = ghy_v.reshape((n_d, -1)) + ghz_v = ghz_v.reshape((n_d, -1)) + gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Phz.T @ ghz_v return f._hDeriv(src, None, gh_v, adjoint=True) diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 9d1be9da3a..cd96b8127a 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -1,4 +1,5 @@ # Test functions +import pytest import unittest import numpy as np from SimPEG import tests, mkvc @@ -12,6 +13,58 @@ MU = mu_0 +@pytest.fixture() +def model_simulation_tuple(): + return nsem.utils.test_utils.setupSimpegNSEM_PrimarySecondary( + nsem.utils.test_utils.halfSpace(1e-2), [0.1], comp="All", singleFreq=False + ) + + +# Test the Jvec derivative +@pytest.mark.parametrize("weights", [True, False]) +def test_Jtjdiag(model_simulation_tuple, weights): + model, simulation = model_simulation_tuple + W = None + if weights: + W = np.eye(simulation.survey.nD) + + J = simulation.getJ(model) + if weights: + J = W @ J + + Jtjdiag = simulation.getJtJdiag(model, W=W) + np.testing.assert_allclose(Jtjdiag, np.sum(J * J, axis=0)) + + +def test_Jtjdiag_clearing(model_simulation_tuple): + model, simulation = model_simulation_tuple + J1 = simulation.getJ(model) + Jtjdiag1 = simulation.getJtJdiag(model) + + m2 = model + 2 + J2 = simulation.getJ(m2) + Jtjdiag2 = simulation.getJtJdiag(m2) + + assert J1 is not J2 + assert Jtjdiag1 is not Jtjdiag2 + + +def test_Jmatrix(model_simulation_tuple): + model, simulation = model_simulation_tuple + rng = np.random.default_rng(4421) + # create random vector + vec = rng.standard_normal(simulation.survey.nD) + + # create the J matrix + J1 = simulation.getJ(model) + Jmatrix_vec = J1.T @ vec + + # compare to JTvec function + jtvec = simulation.Jtvec(model, v=vec) + + np.testing.assert_allclose(Jmatrix_vec, jtvec) + + # Test the Jvec derivative def DerivJvecTest(inputSetup, comp="All", freq=False, expMap=True): m, simulation = nsem.utils.test_utils.setupSimpegNSEM_PrimarySecondary( From 03c71a0c3ef308c64700544c4fa287c613542927 Mon Sep 17 00:00:00 2001 From: Colton Kohnke Date: Wed, 27 Mar 2024 13:52:09 -0400 Subject: [PATCH 376/455] Add LogisticSigmoidMap (#1352) Add a simple bounded Logistic-Sigmoid Mapping as an alternative to the Exponential Map. --------- Co-authored-by: Joseph Capriotti --- SimPEG/__init__.py | 1 + SimPEG/maps.py | 161 ++++++++++++++++++++++++++++++++++++++++ tests/base/test_maps.py | 46 ++++++++++++ 3 files changed, 208 insertions(+) diff --git a/SimPEG/__init__.py b/SimPEG/__init__.py index 18cf134a19..d6ef51cf2a 100644 --- a/SimPEG/__init__.py +++ b/SimPEG/__init__.py @@ -78,6 +78,7 @@ maps.InjectActiveCells maps.MuRelative maps.LogMap + maps.LogisticSigmoidMap maps.ParametricBlock maps.ParametricCircleMap maps.ParametricEllipsoid diff --git a/SimPEG/maps.py b/SimPEG/maps.py index 5cc526b0a0..aa0087100a 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -8,6 +8,7 @@ from scipy.interpolate import UnivariateSpline from scipy.constants import mu_0 from scipy.sparse import csr_matrix as csr +from scipy.special import expit, logit from discretize.tests import check_derivative from discretize import TensorMesh, CylindricalMesh @@ -2156,6 +2157,166 @@ def is_linear(self): return False +class LogisticSigmoidMap(IdentityMap): + r"""Mapping that computes the logistic sigmoid of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``LogisticSigmoidMap`` creates + a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the logistic sigmoid + of every element in :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = sigmoid(\mathbf{m}) = \frac{1}{1+\exp{-\mathbf{m}}} + + ``LogisticSigmoidMap`` transforms values onto the interval (0,1), but can optionally + be scaled and shifted to the interval (a,b). This can be useful for inversion + of data that varies over a log scale and bounded on some interval: + + .. math:: + \mathbf{u}(\mathbf{m}) = a + (b - a) \cdot sigmoid(\mathbf{m}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + lower_bound: float or (nP) numpy.ndarray + lower bound (a) for the transform. Default 0. Defined \in \mathbf{u} space. + upper_bound: float or (nP) numpy.ndarray + upper bound (b) for the transform. Default 1. Defined \in \mathbf{u} space. + + """ + + def __init__(self, mesh=None, nP=None, lower_bound=0, upper_bound=1, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + lower_bound = np.atleast_1d(lower_bound) + upper_bound = np.atleast_1d(upper_bound) + if self.nP != "*": + # check if lower bound and upper bound broadcast to nP + try: + np.broadcast_shapes(lower_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Lower bound does not broadcast to the number of parameters. " + f"Lower bound shape is {lower_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + try: + np.broadcast_shapes(upper_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the number of parameters. " + f"Upper bound shape is {upper_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + # make sure lower and upper bound broadcast to each other... + try: + np.broadcast_shapes(lower_bound.shape, upper_bound.shape) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the lower bound. " + f"Shapes {upper_bound.shape} and {lower_bound.shape} " + f"are incompatible with each other." + ) from err + + if np.any(lower_bound >= upper_bound): + raise ValueError( + "A lower bound is greater than or equal to the upper bound." + ) + + self._lower_bound = lower_bound + self._upper_bound = upper_bound + + @property + def lower_bound(self): + """The lower bound + + Returns + ------- + numpy.ndarray + """ + return self._lower_bound + + @property + def upper_bound(self): + """The upper bound + + Returns + ------- + numpy.ndarray + """ + return self._upper_bound + + def _transform(self, m): + return self.lower_bound + (self.upper_bound - self.lower_bound) * expit(mkvc(m)) + + def inverse(self, m): + r"""Apply the inverse of the mapping to an array. + + For the logistic sigmoid mapping :math:`\mathbf{u}(\mathbf{m})`, the + inverse mapping on a variable :math:`\mathbf{x}` is performed by taking + the log-odds of elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = logit(\mathbf{x}) = \log \frac{\mathbf{x}}{1 - \mathbf{x}} + + or scaled and translated to interval (a,b): + .. math:: + \mathbf{m} = logit(\frac{(\mathbf{x} - a)}{b-a}) + + Parameters + ---------- + m : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + the inverse mapping to the elements in *m*; which in this case + is the log-odds function with scaled and shifted input. + """ + return logit( + (mkvc(m) - self.lower_bound) / (self.upper_bound - self.lower_bound) + ) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` the derivative of the mapping with + respect to the model is a diagonal matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( (b-a)\cdot sigmoid(\mathbf{m})\cdot(1-sigmoid(\mathbf{m})) \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + numpy.ndarray or scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + sigmoid = expit(mkvc(m)) + deriv = (self.upper_bound - self.lower_bound) * sigmoid * (1.0 - sigmoid) + if v is not None: + return deriv * v + return sdiag(deriv) + + @property + def is_linear(self): + return False + + class ChiMap(IdentityMap): r"""Mapping that computes the magnetic permeability given a set of magnetic susceptibilities. diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 9f6c8aaec3..f99cc4b23c 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -564,6 +564,51 @@ def test_Tile(self): self.assertTrue((local_mass - total_mass) / total_mass < 1e-8) + def test_logit_errors(self): + nP = 10 + scalar_lower = -2 + scalar_upper = 2 + good_vector_lower = np.random.rand(nP) - 2 + good_vector_upper = np.random.rand(nP) + 2 + + bad_vector_lower = np.random.rand(nP - 2) - 2 + bad_vector_upper = np.random.rand(nP - 2) + 2 + + # test that lower is not equal to nP + with pytest.raises( + ValueError, + match="Lower bound does not broadcast to the number of parameters.*", + ): + maps.LogisticSigmoidMap( + nP=10, lower_bound=bad_vector_lower, upper_bound=scalar_upper + ) + + # test that bad is not equal to nP + with pytest.raises( + ValueError, + match="Upper bound does not broadcast to the number of parameters.*", + ): + maps.LogisticSigmoidMap( + nP=10, lower_bound=scalar_lower, upper_bound=bad_vector_upper + ) + + # test that two upper and lower arrays will not broadcast when not specifying the number of parameters + with pytest.raises( + ValueError, match="Upper bound does not broadcast to the lower bound.*" + ): + maps.LogisticSigmoidMap( + lower_bound=good_vector_lower, upper_bound=bad_vector_upper + ) + + # test that passing a lower bound higher than an upper bound) + with pytest.raises( + ValueError, + match="A lower bound is greater than or equal to the upper bound.", + ): + maps.LogisticSigmoidMap( + lower_bound=good_vector_upper, upper_bound=good_vector_lower + ) + class TestWires(unittest.TestCase): def test_basic(self): @@ -706,6 +751,7 @@ def test_linearity(): maps.SphericalSystem(mesh2), maps.SelfConsistentEffectiveMedium(mesh2, sigma0=1, sigma1=2), maps.ExpMap(), + maps.LogisticSigmoidMap(), maps.ReciprocalMap(), maps.LogMap(), maps.ParametricCircleMap(mesh2), From 0b57ca900411f327f687ac8afa779174decc9120 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 27 Mar 2024 14:40:33 -0700 Subject: [PATCH 377/455] Remove the cell_weights attribute in regularizations (#1376) Add test checking errors are raised after accessing the property or passing `cell_weights` argument to regularization's constructor. Replace usage of `cell_weights` in examples and code. --------- Co-authored-by: Joseph Capriotti --- .../static/induced_polarization/run.py | 1 - SimPEG/regularization/base.py | 58 ++++++--------- examples/01-maps/plot_sumMap.py | 4 +- examples/08-vrm/plot_inv_vrm_eq.py | 2 +- ...1_PGI_Linear_1D_joint_WithRelationships.py | 4 +- tests/base/test_directives.py | 18 ++++- tests/base/test_regularization.py | 73 +++++++++++-------- 7 files changed, 85 insertions(+), 75 deletions(-) diff --git a/SimPEG/electromagnetics/static/induced_polarization/run.py b/SimPEG/electromagnetics/static/induced_polarization/run.py index 53fbd717f3..a7372118b8 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/run.py +++ b/SimPEG/electromagnetics/static/induced_polarization/run.py @@ -47,7 +47,6 @@ def run_inversion( mesh, active_cells=actind, mapping=regmap, - cell_weights=mesh.cell_volumes[actind], ) reg.alpha_s = alpha_s reg.alpha_x = alpha_x diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index ca38be2e3f..5aaeaedfda 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -1,5 +1,4 @@ from __future__ import annotations -import warnings import numpy as np from discretize.base import BaseMesh @@ -75,18 +74,9 @@ def __init__( "Please use 'active_cells' instead." ) if (key := "cell_weights") in kwargs: - if weights is not None: - raise ValueError( - f"Cannot simultaneously pass 'weights' and '{key}'. " - "Pass 'weights' only." - ) - warnings.warn( - f"The '{key}' argument has been deprecated, please use 'weights'. " - "It will be removed in future versions of SimPEG.", - DeprecationWarning, - stacklevel=2, + raise TypeError( + f"'{key}' argument has been removed. Please use 'weights' instead." ) - weights = kwargs.pop(key) super().__init__(nP=None, mapping=None, **kwargs) self._regularization_mesh = mesh @@ -302,23 +292,19 @@ def regularization_mesh(self) -> RegularizationMesh: @property def cell_weights(self) -> np.ndarray: """Deprecated property for 'volume' and user defined weights.""" - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - stacklevel=2, + raise AttributeError( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." ) - return np.prod(list(self._weights.values()), axis=0) @cell_weights.setter def cell_weights(self, value): - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - stacklevel=2, + raise AttributeError( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." ) - self.set_weights(cell_weights=value) def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. @@ -1583,6 +1569,11 @@ def __init__( "Please use 'active_cells' instead." ) + if (key := "cell_weights") in kwargs: + raise TypeError( + f"'{key}' argument has been removed. Please use 'weights' instead." + ) + self.alpha_s = alpha_s if alpha_x is not None: if length_scale_x is not None: @@ -1714,20 +1705,19 @@ def remove_weights(self, key): @property def cell_weights(self): - # All of the objective functions should have the same weights, - # so just grab the one from smallness here, which should also - # trigger the deprecation warning - return self.objfcts[0].cell_weights + raise AttributeError( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." + ) @cell_weights.setter def cell_weights(self, value): - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - stacklevel=2, + raise AttributeError( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." ) - self.set_weights(cell_weights=value) @property def alpha_s(self): diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index dd7a1d012b..d86976ea08 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -138,7 +138,7 @@ def run(plotIt=True): regMesh = TensorMesh([len(domains)]) reg_m1 = regularization.Sparse(regMesh, mapping=wires.homo) - reg_m1.cell_weights = wires.homo * wr + reg_m1.set_weights(cell_weights=wires.homo * wr) reg_m1.norms = [0, 2] reg_m1.mref = np.zeros(sumMap.shape[1]) @@ -146,7 +146,7 @@ def run(plotIt=True): reg_m2 = regularization.Sparse( mesh, active_cells=actv, mapping=wires.hetero, gradient_type="components" ) - reg_m2.cell_weights = wires.hetero * wr + reg_m2.set_weights(cell_weights=wires.hetero * wr) reg_m2.norms = [0, 0, 0, 0] reg_m2.mref = np.zeros(sumMap.shape[1]) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index aba2d0fde0..e0eee4ff6f 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -196,7 +196,7 @@ w = w / np.max(w) w = w -reg = regularization.Smallness(mesh=mesh, active_cells=actCells, cell_weights=w) +reg = regularization.Smallness(mesh=mesh, active_cells=actCells, weights=w) opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 ) diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index 5da5932952..adabf3f35f 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -235,11 +235,11 @@ def g(k): reg1 = regularization.WeightedLeastSquares( mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m1 ) -reg1.cell_weights = wr1 +reg1.set_weights(cell_weights=wr1) reg2 = regularization.WeightedLeastSquares( mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m2 ) -reg2.cell_weights = wr2 +reg2.set_weights(cell_weights=wr2) reg = reg1 + reg2 opt = optimization.ProjectedGNCG( diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 0d2f2f105c..2af5c58e32 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -161,7 +161,11 @@ def test_sensitivity_weighting_global(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) @@ -201,7 +205,11 @@ def test_sensitivity_weighting_percentile_maximum(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) @@ -241,7 +249,11 @@ def test_sensitivity_weighting_amplitude_minimum(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index 207e8d9cc8..c50a4a83a2 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -758,44 +758,13 @@ def test_multiple_weights(self, mesh, regularization_class): assert reg.weights_keys == ["dummy_weight", "other_weights", "volume"] -class TestDeprecatedArguments: - """ - Test errors after simultaneously passing new and deprecated arguments. - - Within these arguments are: - - * ``cell_weights`` (replaced by ``weights``) - - """ - - @pytest.fixture(params=["1D", "2D", "3D"]) - def mesh(self, request): - """Sample mesh.""" - if request.param == "1D": - hx = np.random.rand(10) - h = [hx / hx.sum()] - elif request.param == "2D": - hx, hy = np.random.rand(10), np.random.rand(9) - h = [h_i / h_i.sum() for h_i in (hx, hy)] - elif request.param == "3D": - hx, hy, hz = np.random.rand(10), np.random.rand(9), np.random.rand(8) - h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] - return discretize.TensorMesh(h) - - def test_weights(self, mesh): - """Test cell_weights and weights.""" - weights = np.ones(len(mesh)) - msg = "Cannot simultaneously pass 'weights' and 'cell_weights'." - with pytest.raises(ValueError, match=msg): - BaseRegularization(mesh, weights=weights, cell_weights=weights) - - class TestRemovedObjects: """ Test if errors are raised after passing removed arguments or trying to access removed properties. * ``indActive`` (replaced by ``active_cells``) + * ``cell_weights`` (replaced by ``weights``) """ @@ -839,6 +808,46 @@ def test_ind_active_property(self, mesh, regularization_class): with pytest.raises(NotImplementedError, match=msg): reg.indActive + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + def test_cell_weights_argument(self, mesh, regularization_class): + """Test if error is raised when passing the cell_weights argument.""" + weights = np.ones(len(mesh)) + msg = "'cell_weights' argument has been removed. Please use 'weights' instead." + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, cell_weights=weights) + + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + def test_cell_weights_property(self, mesh, regularization_class): + """Test if error is raised when trying to access the cell_weights property.""" + weights = {"weights": np.ones(len(mesh))} + msg = ( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." + ) + reg = regularization_class(mesh, weights=weights) + with pytest.raises(AttributeError, match=msg): + reg.cell_weights + + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + def test_cell_weights_setter(self, mesh, regularization_class): + """Test if error is raised when trying to set the cell_weights property.""" + msg = ( + "'cell_weights' has been removed. " + "Please access weights using the `set_weights`, `get_weights`, and " + "`remove_weights` methods." + ) + reg = regularization_class(mesh) + with pytest.raises(AttributeError, match=msg): + reg.cell_weights = "dummy variable" + class TestRemovedRegularizations: """ From 23e27d7c211976c7a64ae203c6ee4688a9f5e590 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 27 Mar 2024 16:31:13 -0700 Subject: [PATCH 378/455] Remove regmesh, mref and gradientType from regularizations (#1377) Remove the `regmesh`, `mref` and `gradientType` properties from regularizations. Make `Sparse` regularizations to raise errors after getting `gradientType` as argument. Add tests checking errors are being raised. Part of the solution to #1302 --- SimPEG/regularization/base.py | 9 ++--- SimPEG/regularization/sparse.py | 33 ++++++++++-------- examples/01-maps/plot_sumMap.py | 4 +-- .../plot_inv_mag_nonLinear_Amplitude.py | 4 +-- .../plot_booky_1D_time_freq_inv.py | 4 +-- .../plot_booky_1Dstitched_resolve_inv.py | 2 +- .../plot_laguna_del_maule_inversion.py | 4 +-- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 3 +- tests/base/test_regularization.py | 34 ++++++++++++++++++- tests/dask/test_mag_MVI_Octree.py | 12 +++---- tests/dask/test_mag_nonLinear_Amplitude.py | 4 +-- tests/pf/test_mag_inversion_linear.py | 2 +- tests/pf/test_mag_inversion_linear_Octree.py | 2 +- tests/pf/test_mag_nonLinear_Amplitude.py | 4 +-- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 2 +- tutorials/07-fdem/plot_inv_1_em1dfm.py | 2 +- tutorials/08-tdem/plot_inv_1_em1dtm.py | 2 +- 17 files changed, 79 insertions(+), 48 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index 5aaeaedfda..b82333025d 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -262,8 +262,7 @@ def reference_model(self, values: np.ndarray | float): "mref", "reference_model", "0.19.0", - future_warn=True, - error=False, + error=True, ) @property @@ -285,8 +284,7 @@ def regularization_mesh(self) -> RegularizationMesh: "regmesh", "regularization_mesh", "0.19.0", - future_warn=True, - error=False, + error=True, ) @property @@ -2111,8 +2109,7 @@ def reference_model(self, values: np.ndarray | float): "mref", "reference_model", "0.19.0", - future_warn=True, - error=False, + error=True, ) @property diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py index 817c49e224..43d5916a03 100644 --- a/SimPEG/regularization/sparse.py +++ b/SimPEG/regularization/sparse.py @@ -577,10 +577,13 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): """ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): - if "gradientType" in kwargs: - self.gradientType = kwargs.pop("gradientType") - else: - self.gradient_type = gradient_type + # Raise error if removed arguments were passed + if (key := "gradientType") in kwargs: + raise TypeError( + f"'{key}' argument has been removed. " + "Please use 'gradient_type' instead." + ) + self.gradient_type = gradient_type super().__init__(mesh=mesh, orientation=orientation, **kwargs) def update_weights(self, m): @@ -695,8 +698,7 @@ def gradient_type(self, value: str): "gradientType", new_name="gradient_type", removal_version="0.19.0", - error=False, - future_warn=True, + error=True, ) @@ -930,6 +932,14 @@ def __init__( f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " f"Value of type {type(mesh)} provided." ) + + # Raise error if removed arguments were passed + if (key := "gradientType") in kwargs: + raise TypeError( + f"'{key}' argument has been removed. " + "Please use 'gradient_type' instead." + ) + self._regularization_mesh = mesh if active_cells is not None: self._regularization_mesh.active_cells = active_cells @@ -950,7 +960,6 @@ def __init__( SparseSmoothness(mesh=self.regularization_mesh, orientation="z") ) - gradientType = kwargs.pop("gradientType", None) super().__init__( self.regularization_mesh, objfcts=objfcts, @@ -959,13 +968,7 @@ def __init__( if norms is None: norms = [1] * (mesh.dim + 1) self.norms = norms - - if gradientType is not None: - # Trigger deprecation warning - self.gradientType = gradientType - else: - self.gradient_type = gradient_type - + self.gradient_type = gradient_type self.irls_scaled = irls_scaled self.irls_threshold = irls_threshold @@ -995,7 +998,7 @@ def gradient_type(self, value: str): self._gradient_type = value gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=False, future_warn=True + gradient_type, "gradientType", "0.19.0", error=True ) @property diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index d86976ea08..fe99b0cc2e 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -140,7 +140,7 @@ def run(plotIt=True): reg_m1 = regularization.Sparse(regMesh, mapping=wires.homo) reg_m1.set_weights(cell_weights=wires.homo * wr) reg_m1.norms = [0, 2] - reg_m1.mref = np.zeros(sumMap.shape[1]) + reg_m1.reference_model = np.zeros(sumMap.shape[1]) # Regularization for the voxel model reg_m2 = regularization.Sparse( @@ -148,7 +148,7 @@ def run(plotIt=True): ) reg_m2.set_weights(cell_weights=wires.hetero * wr) reg_m2.norms = [0, 0, 0, 0] - reg_m2.mref = np.zeros(sumMap.shape[1]) + reg_m2.reference_model = np.zeros(sumMap.shape[1]) reg = reg_m1 + reg_m2 diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index bbcd745dce..a7bf711161 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -235,7 +235,7 @@ reg = regularization.Sparse( mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) -reg.mref = np.zeros(nC) +reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -347,7 +347,7 @@ # Create a sparse regularization reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] -reg.mref = np.zeros(nC) +reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/examples/20-published/plot_booky_1D_time_freq_inv.py b/examples/20-published/plot_booky_1D_time_freq_inv.py index 180685ef32..e1469d3f3d 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -261,7 +261,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): inv = inversion.BaseInversion(invProb, directiveList=[target]) reg.alpha_s = 1e-3 reg.alpha_x = 1.0 - reg.mref = m0.copy() + reg.reference_model = m0.copy() opt.LSshorten = 0.5 opt.remember("xc") # run the inversion @@ -379,7 +379,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): reg.alpha_x = 1.0 opt.LSshorten = 0.5 opt.remember("xc") - reg.mref = mopt_re # Use RESOLVE model as a reference model + reg.reference_model = mopt_re # Use RESOLVE model as a reference model # run the inversion mopt_sky = inv.run(m0) diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index fc10a3317c..dbdff9966b 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -127,7 +127,7 @@ def resolve_1Dinversions( # regularization regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) reg = regularization.WeightedLeastSquares(regMesh) - reg.mref = mref + reg.reference_model = mref # optimization opt = optimization.InexactGaussNewton(maxIter=10) diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index 0edd61c3c2..d124efa9da 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -96,9 +96,9 @@ def run(plotIt=True, cleanAfterRun=True): # %% Create inversion objects reg = regularization.Sparse( - mesh, active_cells=active, mapping=staticCells, gradientType="total" + mesh, active_cells=active, mapping=staticCells, gradient_type="total" ) - reg.mref = driver.mref[dynamic] + reg.reference_model = driver.mref[dynamic] reg.norms = [0.0, 1.0, 1.0, 1.0] # reg.norms = driver.lpnorms diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index b6c67f0fb3..8c3238670b 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -155,9 +155,8 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): # Related to inversion reg = regularization.Sparse( - mesh, active_cells=actind, mapping=regmap, gradientType="components" + mesh, active_cells=actind, mapping=regmap, gradient_type="components" ) - # gradientType = 'components' reg.norms = [p, qx, qz, 0.0] IRLS = directives.Update_IRLS( max_irls_iterations=20, minGNiter=1, beta_search=False, fix_Jmatrix=True diff --git a/tests/base/test_regularization.py b/tests/base/test_regularization.py index c50a4a83a2..4150aba4f9 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/test_regularization.py @@ -9,6 +9,8 @@ from SimPEG.regularization import ( BaseRegularization, WeightedLeastSquares, + Sparse, + SparseSmoothness, Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder, @@ -91,7 +93,7 @@ def test_regularization(self): else: m = np.random.rand(mesh.nC) mref = np.ones_like(m) * np.mean(m) - reg.mref = mref + reg.reference_model = mref # test derivs passed = reg.test(m, eps=TOL) @@ -764,6 +766,9 @@ class TestRemovedObjects: access removed properties. * ``indActive`` (replaced by ``active_cells``) + * ``gradientType`` (replaced by ``gradient_type``) + * ``mref`` (replaced by ``reference_model``) + * ``regmesh`` (replaced by ``regularization_mesh``) * ``cell_weights`` (replaced by ``weights``) """ @@ -782,6 +787,33 @@ def mesh(self, request): h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] return discretize.TensorMesh(h) + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + def test_mref_property(self, mesh, regularization_class): + """Test mref property.""" + msg = "mref has been removed, please use reference_model." + reg = regularization_class(mesh) + with pytest.raises(NotImplementedError, match=msg): + reg.mref + + def test_regmesh_property(self, mesh): + """Test regmesh property.""" + msg = "regmesh has been removed, please use regularization_mesh." + reg = BaseRegularization(mesh) + with pytest.raises(NotImplementedError, match=msg): + reg.regmesh + + @pytest.mark.parametrize("regularization_class", (Sparse, SparseSmoothness)) + def test_gradient_type(self, mesh, regularization_class): + """Test gradientType argument.""" + msg = ( + "'gradientType' argument has been removed. " + "Please use 'gradient_type' instead." + ) + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, gradientType="total") + @pytest.mark.parametrize( "regularization_class", (BaseRegularization, WeightedLeastSquares), diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 3c7305f552..37b8783745 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -118,16 +118,16 @@ def setUp(self): # Create three regularization for the different components # of magnetization reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.p) - reg_p.mref = np.zeros(3 * nC) + reg_p.reference_model = np.zeros(3 * nC) reg_s = regularization.Sparse(mesh, active_cells=actv, mapping=wires.s) - reg_s.mref = np.zeros(3 * nC) + reg_s.reference_model = np.zeros(3 * nC) reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.t) - reg_t.mref = np.zeros(3 * nC) + reg_t.reference_model = np.zeros(3 * nC) reg = reg_p + reg_s + reg_t - reg.mref = np.zeros(3 * nC) + reg.reference_model = np.zeros(3 * nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) @@ -173,7 +173,7 @@ def setUp(self): # Regularize the amplitude of the vectors reg_a = regularization.Sparse(mesh, active_cells=actv, mapping=wires.amp) reg_a.norms = [0.0, 0.0, 0.0, 0.0] # Sparse on the model and its gradients - reg_a.mref = np.zeros(3 * nC) + reg_a.reference_model = np.zeros(3 * nC) # Regularize the vertical angle of the vectors reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.theta) @@ -188,7 +188,7 @@ def setUp(self): reg_p.norms = [2.0, 0.0, 0.0, 0.0] # Only norm on gradients used reg = reg_a + reg_t + reg_p - reg.mref = np.zeros(3 * nC) + reg.reference_model = np.zeros(3 * nC) Lbound = np.kron(np.asarray([0, -np.inf, -np.inf]), np.ones(nC)) Ubound = np.kron(np.asarray([10, np.inf, np.inf]), np.ones(nC)) diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 1f9109d402..758db82d0a 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -144,7 +144,7 @@ def setUp(self): reg = regularization.Sparse( mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -237,7 +237,7 @@ def setUp(self): # Create a sparse regularization reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 2da13bf2f5..bf7e10dba8 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -103,7 +103,7 @@ def setUp(self): # Create a regularization reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] - reg.gradientType = "components" + reg.gradient_type = "components" # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index 64ce31e0bd..8167bf1e1f 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -117,7 +117,7 @@ def setUp(self): # Create a regularization reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index 015aa8bfe0..85f27266d6 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -143,7 +143,7 @@ def setUp(self): reg = regularization.Sparse( mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -236,7 +236,7 @@ def setUp(self): # Create a sparse regularization reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index 50ad50e100..d72de43fb9 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -305,7 +305,7 @@ active_cells=ind_active, reference_model=starting_conductivity_model, mapping=regmap, - gradientType="total", + gradient_type="total", alpha_s=0.01, alpha_x=1, alpha_y=1, diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index 8c58fc7adc..2d566f06a7 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -238,7 +238,7 @@ reg = regularization.Sparse(mesh, mapping=reg_map, alpha_s=0.025, alpha_x=1.0) # reference model -reg.mref = starting_model +reg.reference_model = starting_model # Define sparse and blocky norms p, q reg.norms = [0, 0] diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index ae188747c0..535f646747 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -227,7 +227,7 @@ reg = regularization.Sparse(mesh, mapping=reg_map, alpha_s=0.01, alpha_x=1.0) # set reference model -reg.mref = starting_model +reg.reference_model = starting_model # Define sparse and blocky norms p, q reg.norms = [1, 0] From 6b9c6baeadc8f35e58f6eb101efd155ba279d402 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 27 Mar 2024 17:09:10 -0700 Subject: [PATCH 379/455] Test if gravity sensitivities are stored on disk (#1388) Add a test function checking that the sensitivity matrix in the gravity simulation using Choclo as engine is being stored in disk using a Numpy memmap. --- tests/pf/test_forward_Grav_Linear.py | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 7208f0f9e3..e65c7f7803 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -359,6 +359,47 @@ def test_choclo_and_sensitivity_path_as_dir(self, simple_mesh, tmp_path): engine="choclo", ) + def test_sensitivities_on_disk(self, simple_mesh, receivers_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being stored in disk when asked + """ + # Build survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Build simulation + sensitivities_path = tmp_path / "sensitivities" + simulation = gravity.Simulation3DIntegral( + mesh=simple_mesh, + survey=survey, + store_sensitivities="disk", + sensitivity_path=str(sensitivities_path), + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix was stored in disk and is a memmap + assert sensitivities_path.is_file() + assert type(simulation.G) is np.memmap + + def test_sensitivities_on_ram(self, simple_mesh, receivers_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being allocated in memory when asked + """ + # Build survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Build simulation + simulation = gravity.Simulation3DIntegral( + mesh=simple_mesh, + survey=survey, + store_sensitivities="ram", + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix is a Numpy array (stored in memory) + assert type(simulation.G) is np.ndarray + def test_choclo_missing(self, simple_mesh, monkeypatch): """ Check if error is raised when choclo is missing and chosen as engine. From 6952d9015413c6e6ccee541204cc130effefe210 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 28 Mar 2024 10:44:22 -0700 Subject: [PATCH 380/455] Check if mesh is 3D when using Choclo in gravity simulation (#1386) Make gravity simulation to raise an error if the engine is `"choclo"` and the passed mesh is not 3d. Add a test to check the error. --- SimPEG/potential_fields/gravity/simulation.py | 8 ++++- tests/pf/test_forward_Grav_Linear.py | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py index 434c6992dc..a520019939 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/SimPEG/potential_fields/gravity/simulation.py @@ -131,8 +131,14 @@ def __init__( self.numba_parallel = numba_parallel self.engine = engine self._sanity_checks_engine(kwargs) - # Define jit functions if self.engine == "choclo": + # Check dimensions of the mesh + if self.mesh.dim != 3: + raise ValueError( + f"Invalid mesh with {self.mesh.dim} dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + # Define jit functions if numba_parallel: self._sensitivity_gravity = _sensitivity_gravity_parallel self._forward_gravity = _forward_gravity_parallel diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index e65c7f7803..5e7d865f6b 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -436,3 +436,34 @@ def test_invalid_conversion_factor(self): component = "invalid-component" with pytest.raises(ValueError, match=f"Invalid component '{component}'"): gravity.simulation._get_conversion_factor(component) + + +class TestInvalidMeshChoclo: + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = discretize.TensorMesh(h, "CC") + else: + mesh = discretize.TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + # Build survey + receivers_locations = np.array([[0, 0, 0]]) + receivers = gravity.Point(receivers_locations) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Check if error is raised + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + with pytest.raises(ValueError, match=msg): + gravity.Simulation3DIntegral(mesh, survey, engine="choclo") From 3ba9a4b46734f81321486a8c20844b4de7769b4a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 28 Mar 2024 13:37:01 -0600 Subject: [PATCH 381/455] Rotated Gradients (#1167) Constructs an (optionally) anisotropic Smoothness Measure operator on general meshes. This makes use of discretize's `get_face_inner_product` operator to construct the regularization as an inner product of cell gradients. Essentially the weighting parameters are an anisotropic model used to combine the gradients. --- SimPEG/regularization/__init__.py | 2 + SimPEG/regularization/_gradient.py | 271 ++++++++++++++++++ SimPEG/regularization/regularization_mesh.py | 6 +- .../test_cross_gradient.py | 15 +- .../regularizations/test_full_gradient.py | 236 +++++++++++++++ tests/base/{ => regularizations}/test_jtv.py | 0 .../test_pgi_regularization.py | 0 .../test_regularization.py | 3 +- 8 files changed, 528 insertions(+), 5 deletions(-) create mode 100644 SimPEG/regularization/_gradient.py rename tests/base/{ => regularizations}/test_cross_gradient.py (95%) create mode 100644 tests/base/regularizations/test_full_gradient.py rename tests/base/{ => regularizations}/test_jtv.py (100%) rename tests/base/{ => regularizations}/test_pgi_regularization.py (100%) rename tests/base/{ => regularizations}/test_regularization.py (99%) diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 334e4ed986..3fa55b9fd3 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -90,6 +90,7 @@ Smallness SmoothnessFirstOrder SmoothnessSecondOrder + SmoothnessFullGradient Sparse Norm Regularization -------------------------- @@ -169,6 +170,7 @@ AmplitudeSmallness, AmplitudeSmoothnessFirstOrder, ) +from ._gradient import SmoothnessFullGradient @deprecate_class(removal_version="0.19.0", error=True) diff --git a/SimPEG/regularization/_gradient.py b/SimPEG/regularization/_gradient.py new file mode 100644 index 0000000000..7e98309f48 --- /dev/null +++ b/SimPEG/regularization/_gradient.py @@ -0,0 +1,271 @@ +from .base import BaseRegularization +import numpy as np +import scipy.sparse as sp +from ..utils.code_utils import validate_ndarray_with_shape + + +class SmoothnessFullGradient(BaseRegularization): + r"""Measures the gradient of a model using optionally anisotropic weighting. + + This regularizer measures the first order smoothness in a mesh ambivalent way + by observing that the N-d smoothness operator can be represented as an + inner product with an arbitrarily anisotropic weight. + + By default it assumes uniform weighting in each dimension, which works + for most ``discretize`` mesh types. + + Parameters + ---------- + mesh : discretize.BaseMesh + The mesh object to use for regularization. The mesh should either have + a `cell_gradient` or a `stencil_cell_gradient` defined. + alphas : (mesh.dim,) or (mesh.n_cells, mesh.dim) array_like of float, optional. + The weights of the regularization for each axis. This can be defined for each cell + in the mesh. Default is uniform weights equal to the smallest edge length squared. + reg_dirs : (mesh.dim, mesh.dim) or (mesh.n_cells, mesh.dim, mesh.dim) array_like of float + Matrix or list of matrices whose columns represent the regularization directions. + Each matrix should be orthonormal. Default is Identity. + ortho_check : bool, optional + Whether to check `reg_dirs` for orthogonality. + **kwargs + Keyword arguments passed to the parent class ``BaseRegularization``. + + Examples + -------- + Construct of 2D measure with uniform smoothing in each direction. + + >>> from discretize import TensorMesh + >>> from SimPEG.regularization import SmoothnessFullGradient + >>> mesh = TensorMesh([32, 32]) + >>> reg = SmoothnessFullGradient(mesh) + + We can instead create a measure that smooths twice as much in the 1st dimension + than it does in the second dimension. + >>> reg = SmoothnessFullGradient(mesh, [2, 1]) + + The `alphas` parameter can also be indepenant for each cell. Here we set all cells + lower than 0.5 in the x2 to twice as much in the first dimension + otherwise it is uniform smoothing. + >>> alphas = np.ones((mesh.n_cells, mesh.dim)) + >>> alphas[mesh.cell_centers[:, 1] < 0.5] = [2, 1] + >>> reg = SmoothnessFullGradient(mesh, alphas) + + We can also rotate the axis in which we want to preferentially smooth. Say we want to + smooth twice as much along the +x1,+x2 diagonal as we do along the -x1,+x2 diagonal, + effectively rotating our smoothing 45 degrees. Note and the columns of the matrix + represent the directional vectors (not the rows). + >>> sqrt2 = np.sqrt(2) + >>> reg_dirs = np.array([ + ... [sqrt2, -sqrt2], + ... [sqrt2, sqrt2], + ... ]) + >>> reg = SmoothnessFullGradient(mesh, alphas, reg_dirs=reg_dirs) + + Notes + ----- + The regularization object is the discretized form of the continuous regularization + + ..math: + f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \hspace{5pt} \partial V + + The tensor quantity `a` is used to represent the potential preferential directions of + regularization. `a` must be symmetric positive semi-definite with an eigendecomposition of: + + ..math: + \mathbf{a} = \mathbf{Q}\mathbf{L}\mathbf{Q}^{-1} + + `Q` is then the regularization directions ``reg_dirs``, and `L` is represents the weighting + along each direction, with ``alphas`` along its diagonal. These are multiplied to form the + anisotropic alpha used for rotated gradients. + """ + + def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs): + if mesh.dim < 2: + raise TypeError("Mesh must have dimension higher than 1") + super().__init__(mesh=mesh, **kwargs) + + if alphas is None: + edge_length = np.min(mesh.edge_lengths) + alphas = edge_length**2 * np.ones(mesh.dim) + alphas = validate_ndarray_with_shape( + "alphas", + alphas, + shape=[(mesh.dim,), ("*", mesh.dim)], + dtype=float, + ) + n_active_cells = self.regularization_mesh.n_cells + if len(alphas.shape) == 1: + alphas = np.tile(alphas, (mesh.n_cells, 1)) + if alphas.shape[0] != mesh.n_cells: + # check if I need to expand from active cells to all cells (needed for discretize) + if self.active_cells is not None and alphas.shape[0] == n_active_cells: + alpha_temp = np.zeros((mesh.n_cells, mesh.dim)) + alpha_temp[self.active_cells] = alphas + alphas = alpha_temp + else: + raise IndexError( + f"`alphas` first dimension, {alphas.shape[0]}, must be either number " + f"of active cells {n_active_cells}, or the number of mesh cells {mesh.n_cells}. " + ) + if np.any(alphas < 0): + raise ValueError("`alpha` must be non-negative") + anis_alpha = alphas + + if reg_dirs is not None: + reg_dirs = validate_ndarray_with_shape( + "reg_dirs", + reg_dirs, + shape=[(mesh.dim, mesh.dim), ("*", mesh.dim, mesh.dim)], + dtype=float, + ) + if reg_dirs.shape == (mesh.dim, mesh.dim): + reg_dirs = np.tile(reg_dirs, (mesh.n_cells, 1, 1)) + if reg_dirs.shape[0] != mesh.n_cells: + # check if I need to expand from active cells to all cells (needed for discretize) + if ( + self.active_cells is not None + and reg_dirs.shape[0] == n_active_cells + ): + reg_dirs_temp = np.zeros((mesh.n_cells, mesh.dim, mesh.dim)) + reg_dirs_temp[self.active_cells] = reg_dirs + reg_dirs = reg_dirs_temp + else: + raise IndexError( + f"`reg_dirs` first dimension, {reg_dirs.shape[0]}, must be either number " + f"of active cells {n_active_cells}, or the number of mesh cells {mesh.n_cells}. " + ) + # check orthogonality? + if ortho_check: + eye = np.eye(mesh.dim) + for i, M in enumerate(reg_dirs): + if not np.allclose(eye, M @ M.T): + raise ValueError(f"Matrix {i} is not orthonormal") + # create a stack of matrices of dir @ alphas @ dir.T + anis_alpha = np.einsum("ink,ik,imk->inm", reg_dirs, anis_alpha, reg_dirs) + # Then select the upper diagonal components for input to discretize + if mesh.dim == 2: + anis_alpha = np.stack( + ( + anis_alpha[..., 0, 0], + anis_alpha[..., 1, 1], + anis_alpha[..., 0, 1], + ), + axis=-1, + ) + elif mesh.dim == 3: + anis_alpha = np.stack( + ( + anis_alpha[..., 0, 0], + anis_alpha[..., 1, 1], + anis_alpha[..., 2, 2], + anis_alpha[..., 0, 1], + anis_alpha[..., 0, 2], + anis_alpha[..., 1, 2], + ), + axis=-1, + ) + self._anis_alpha = anis_alpha + + # overwrite the call, deriv, and deriv2... + def __call__(self, m): + G = self.cell_gradient + M_f = self.W + r = G @ (self.mapping * (self._delta_m(m))) + return r @ M_f @ r + + def deriv(self, m): + m_d = self.mapping.deriv(self._delta_m(m)) + G = self.cell_gradient + M_f = self.W + r = G @ (self.mapping * (self._delta_m(m))) + return 2 * (m_d.T * (G.T @ (M_f @ r))) + + def deriv2(self, m, v=None): + m_d = self.mapping.deriv(self._delta_m(m)) + G = self.cell_gradient + M_f = self.W + if v is None: + return 2 * (m_d.T @ (G.T @ M_f @ G) @ m_d) + + return 2 * (m_d.T @ (G.T @ (M_f @ (G @ (m_d @ v))))) + + @property + def cell_gradient(self): + """The (approximate) cell gradient operator + + Returns + ------- + scipy.sparse.csr_matrix + """ + if getattr(self, "_cell_gradient", None) is None: + mesh = self.regularization_mesh.mesh + try: + cell_gradient = mesh.cell_gradient + except AttributeError: + a = mesh.face_areas + v = mesh.average_cell_to_face @ mesh.cell_volumes + cell_gradient = sp.diags(a / v) @ mesh.stencil_cell_gradient + + v = np.ones(mesh.n_cells) + # Turn off cell_gradient at boundary faces + if self.active_cells is not None: + v[~self.active_cells] = 0 + + dv = cell_gradient @ v + P = sp.diags((np.abs(dv) <= 1e-16).astype(int)) + cell_gradient = P @ cell_gradient + if self.active_cells is not None: + cell_gradient = cell_gradient[:, self.active_cells] + self._cell_gradient = cell_gradient + return self._cell_gradient + + @property + def _weights_shapes(self): + reg_mesh = self.regularization_mesh + mesh = reg_mesh.mesh + return [(mesh.n_faces,), (reg_mesh.n_cells,)] + + @property + def W(self): + """The inner product operator using rotated coordinates + + Returns + ------- + scipy.sparse.csr_matrix + + Notes + ----- + This matrix is equivalent to `W.T @ W` in most other regularizations. It uses + `discretize` inner product operators to form the matrix `W.T @ W` all at once. + """ + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh.mesh + n_faces = mesh.n_faces + n_cells = self.regularization_mesh.n_cells + cell_weights = np.ones(n_cells) + face_weights = np.ones(n_faces) + for values in self._weights.values(): + if len(values) == n_cells: + cell_weights *= values + elif len(values) == n_faces: + face_weights *= values + else: + raise ValueError( + "Weights must be either number of active cells, or number of total faces" + ) + # optionally expand the cell weights if there are inactive cells + if n_cells != len(mesh) and self.active_cells is not None: + weights = np.zeros(mesh.n_cells) + weights[self.active_cells] = cell_weights + cell_weights = weights + reg_model = self._anis_alpha * cell_weights[:, None] + # turn off measure in inactive cells + if self.active_cells is not None: + reg_model[~self.active_cells] = 0.0 + + Wf = sp.diags(np.sqrt(face_weights)) + + W = mesh.get_face_inner_product(reg_model) + + self._W = Wf @ (W @ Wf) + return self._W diff --git a/SimPEG/regularization/regularization_mesh.py b/SimPEG/regularization/regularization_mesh.py index 713ed8b630..63eaa981f2 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/SimPEG/regularization/regularization_mesh.py @@ -111,7 +111,7 @@ def vol(self) -> np.ndarray: return self._vol @property - def nC(self) -> int: + def n_cells(self) -> int: """Number of active cells. Returns @@ -121,7 +121,9 @@ def nC(self) -> int: """ if self.active_cells is not None: return int(self.active_cells.sum()) - return self.mesh.nC + return self.mesh.n_cells + + nC = n_cells @property def dim(self) -> int: diff --git a/tests/base/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py similarity index 95% rename from tests/base/test_cross_gradient.py rename to tests/base/regularizations/test_cross_gradient.py index b0493b8569..907f04bb56 100644 --- a/tests/base/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -8,8 +8,6 @@ regularization, ) -np.random.seed(10) - class CrossGradientTensor2D(unittest.TestCase): def setUp(self): @@ -42,6 +40,7 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True self.assertTrue(cross_grad.test()) @@ -52,12 +51,14 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False self.assertTrue(cross_grad._test_deriv()) self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) def test_deriv2_no_arg(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -134,6 +135,7 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True self.assertTrue(cross_grad.test()) @@ -144,12 +146,14 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False self.assertTrue(cross_grad._test_deriv()) self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) def test_deriv2_no_arg(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -167,6 +171,7 @@ def test_deriv2_no_arg(self): np.testing.assert_allclose(Wv, W @ v) def test_cross_grad_calc(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -209,6 +214,7 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True self.assertTrue(cross_grad.test()) @@ -219,12 +225,14 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False self.assertTrue(cross_grad._test_deriv()) self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) def test_deriv2_no_arg(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -274,6 +282,7 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True self.assertTrue(cross_grad.test()) @@ -284,12 +293,14 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ + np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False self.assertTrue(cross_grad._test_deriv()) self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) def test_deriv2_no_arg(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad diff --git a/tests/base/regularizations/test_full_gradient.py b/tests/base/regularizations/test_full_gradient.py new file mode 100644 index 0000000000..a827676fc8 --- /dev/null +++ b/tests/base/regularizations/test_full_gradient.py @@ -0,0 +1,236 @@ +from discretize.tests import assert_expected_order, check_derivative +from discretize.utils import example_simplex_mesh +import discretize +import numpy as np +from SimPEG.regularization import SmoothnessFullGradient +import pytest + + +def f_2d(x, y): + return (1 - np.cos(2 * x * np.pi)) * (1 - np.cos(4 * y * np.pi)) + + +def f_3d(x, y, z): + return f_2d(x, y) * (1 - np.cos(8 * z * np.pi)) + + +dir_2d = np.array([[1.0, 1.0], [-1.0, 1.0]]).T +dir_2d /= np.linalg.norm(dir_2d, axis=0) +dir_3d = np.array([[1, 1, 1], [-1, 1, 0], [-1, -1, 2]]).T +dir_3d = dir_3d / np.linalg.norm(dir_3d, axis=0) + +# a list of argument tuples to pass to pytest parameterize +# each is a tuple of (function, dim, true_value, alphas, reg_dirs) +parameterized_args = [ + (f_2d, 2, 15 * np.pi**2, [1, 1], None), # assumes reg_dirs aligned with axes + ( + f_2d, + 2, + 15 * np.pi**2, + [1, 1], + np.eye(2), + ), # test for explicitly aligned with axes + ( + f_2d, + 2, + 15 * np.pi**2, + [1, 1], + dir_2d, + ), # circular regularization should be invariant to rotation + ( + f_2d, + 2, + 27 * np.pi**2, + [1, 2], + None, + ), # elliptic regularization aligned with axes + (f_2d, 2, 111.033049512255 * 2, [1, 2], dir_2d), # rotated elliptic regularization + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + None, + ), # test for explicitly aligned with axes + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + np.eye(3), + ), # test for explicitly aligned with axes + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + dir_3d, + ), # circular regularization should be invariant to rotation + ( + f_3d, + 3, + 513 * np.pi**2 / 2, + [1, 2, 3], + None, + ), # elliptic regularization aligned with axes + ( + f_3d, + 3, + 1065.91727531765 * 2, + [1, 2, 3], + dir_3d, + ), # rotated elliptic regularization +] + + +@pytest.mark.parametrize("mesh_class", [discretize.TensorMesh, discretize.TreeMesh]) +@pytest.mark.parametrize("func,dim,true_value,alphas,reg_dirs", parameterized_args) +def test_regulariation_order(mesh_class, func, dim, true_value, alphas, reg_dirs): + """This function is testing for the accuracy of the regularization. + Basically, is it actually measuring what we say it's measuring. + """ + n_hs = [8, 16, 32] + + def reg_error(n): + h = [n] * dim + mesh = mesh_class(h) + if mesh_class is discretize.TreeMesh: + mesh.refine(-1) + # cell widths will be the same in each dimension + dh = mesh.h[0][0] + + f_eval = func(*mesh.cell_centers.T) + + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=reg_dirs) + + numerical_eval = reg(f_eval) + err = np.abs(numerical_eval - true_value) + return err, dh + + assert_expected_order(reg_error, n_hs) + + +@pytest.mark.parametrize("dim", [2, 3]) +def test_simplex_mesh(dim): + """Test to make sure it works with a simplex mesh + + We can't make as strong of an accuracy claim for this mesh type because the cell gradient + operator is not actually defined for it (it uses an approximation to the cell gradient). + It is close, but we should at least test that it works.. + """ + h = [10] * dim + points, simplices = example_simplex_mesh(h) + mesh = discretize.SimplexMesh(points, simplices) + reg = SmoothnessFullGradient(mesh) + + # multiply it by a vector to make sure we can construct everything internally + # at the very least, we should be able to confirm it evaluates to 0 for a flat model. + out = reg(np.ones(mesh.n_cells)) + np.testing.assert_allclose(out, 0) + + +@pytest.mark.parametrize( + "dim,alphas,reg_dirs", [(2, [1, 2], dir_2d), (3, [1, 2, 3], dir_3d)] +) +def test_first_derivatives(dim, alphas, reg_dirs): + """Perform a derivative test.""" + h = [10] * dim + mesh = discretize.TensorMesh(h) + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=reg_dirs) + + def func(x): + return reg(x), reg.deriv(x) + + check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + + +@pytest.mark.parametrize( + "dim,alphas,reg_dirs", [(2, [1, 2], dir_2d), (3, [1, 2, 3], dir_3d)] +) +def test_second_derivatives(dim, alphas, reg_dirs): + """Perform a derivative test.""" + h = [10] * dim + mesh = discretize.TensorMesh(h) + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=reg_dirs) + + def func(x): + return reg.deriv(x), lambda v: reg.deriv2(x, v) + + check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + + +@pytest.mark.parametrize("with_active_cells", [True, False]) +def test_operations(with_active_cells, dim=3): + # Here we just make sure operations at least work + h = [10] * dim + mesh = discretize.TensorMesh(h) + if with_active_cells: + active_cells = mesh.cell_centers[:, -1] <= 0.75 + n_cells = active_cells.sum() + else: + active_cells = None + n_cells = mesh.n_cells + reg = SmoothnessFullGradient(mesh, active_cells=active_cells) + # create a model + m = np.arange(n_cells) + # create a vector + v = np.random.rand(n_cells) + # test the second derivative evaluates + # and gives same results with and without a vector + v1 = reg.deriv2(m, v) + v2 = reg.deriv2(m) @ v + np.testing.assert_allclose(v1, v2) + + W1 = reg.W + + # test assigning n_cells + reg.set_weights(temp_weight=np.random.rand(n_cells)) + + # setting a weight should've erased W + assert reg._W is None + + # test assigning n_total_faces face weight + reg.set_weights(temp_weight=np.random.rand(mesh.n_faces)) + + # and test it all works! + W2 = reg.W + assert W1 is not W2 + + +def test_errors(): + # bad dimension mesh + mesh1d = discretize.TensorMesh([5]) + with pytest.raises(TypeError): + SmoothnessFullGradient(mesh1d) + mesh2d = discretize.TensorMesh([5, 5]) + # test some bad alphas + with pytest.raises(ValueError): + # 3D alpha passed to 2D operator + SmoothnessFullGradient(mesh2d, [1, 2, 3]) + + with pytest.raises(IndexError): + # incorrect number cell dependent alphas + alphas = np.random.rand(mesh2d.n_cells - 5, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas) + + with pytest.raises(ValueError): + # negative alphas + SmoothnessFullGradient(mesh2d, [-1, 1, 1]) + + alphas = [1, 2] + # test some bad reg dirs + with pytest.raises(ValueError): + # 3D reg dirs to 2D reg + reg_dirs = np.random.rand(3, 3) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) + + with pytest.raises(IndexError): + # incorrect number of cell dependent reg_dirs + reg_dirs = np.random.rand(mesh2d.n_cells - 5, 2, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) + + with pytest.raises(ValueError): + # non orthnormal reg_dirs + # incorrect number of cell dependent reg_dirs + reg_dirs = np.random.rand(2, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) diff --git a/tests/base/test_jtv.py b/tests/base/regularizations/test_jtv.py similarity index 100% rename from tests/base/test_jtv.py rename to tests/base/regularizations/test_jtv.py diff --git a/tests/base/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py similarity index 100% rename from tests/base/test_pgi_regularization.py rename to tests/base/regularizations/test_pgi_regularization.py diff --git a/tests/base/test_regularization.py b/tests/base/regularizations/test_regularization.py similarity index 99% rename from tests/base/test_regularization.py rename to tests/base/regularizations/test_regularization.py index 4150aba4f9..5fc7773ce6 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -39,6 +39,7 @@ "LinearCorrespondence", "JointTotalVariation", "BaseAmplitude", + "SmoothnessFullGradient", "VectorAmplitude", "CrossReferenceRegularization", # Removed regularization classes that raise error on instantiation @@ -181,7 +182,7 @@ def test_property_mirroring(self): active_cells = mesh.gridCC[:, 2] < 0.6 reg = getattr(regularization, regType)(mesh, active_cells=active_cells) - self.assertTrue(reg.nP == reg.regularization_mesh.nC) + self.assertTrue(reg.nP == reg.regularization_mesh.n_cells) [ self.assertTrue(np.all(fct.active_cells == active_cells)) From 4c4a2d9727ed6204d1a525c7a5d9aaa72a0b145d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 28 Mar 2024 12:39:36 -0700 Subject: [PATCH 382/455] Add directives to the API Reference (#1397) List directives in the API Reference under their own "Directives" category. Fixes #1396 --- SimPEG/directives/__init__.py | 98 ++++++++++++++++++++++++++ docs/content/api/SimPEG.directives.rst | 1 + docs/content/api/index.rst | 7 ++ 3 files changed, 106 insertions(+) create mode 100644 docs/content/api/SimPEG.directives.rst diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py index a713648113..34c8c1fee7 100644 --- a/SimPEG/directives/__init__.py +++ b/SimPEG/directives/__init__.py @@ -1,3 +1,101 @@ +""" +============================================= +Directives (:mod:`SimPEG.directives`) +============================================= + +.. currentmodule:: SimPEG.directives + +Directives are classes that allow us to control the inversion, perform tasks +between iterations, save information about our inversion process and more. +Directives are passed to the ``SimPEG.inversion.BaseInversion`` class through +the ``directiveList`` argument. The tasks specified through the directives are +executed after each inversion iteration, following the same order as in which +they are passed in the ``directiveList``. + +Although you can write your own directive classes and plug them into your +inversion, we provide a set of useful directive classes that cover a wide range +of applications: + + +General purpose directives +========================== + +.. autosummary:: + :toctree: generated/ + + AlphasSmoothEstimate_ByEig + BetaEstimateMaxDerivative + BetaEstimate_ByEig + BetaSchedule + JointScalingSchedule + MultiTargetMisfits + ProjectSphericalBounds + ScalingMultipleDataMisfits_ByEig + TargetMisfit + UpdatePreconditioner + UpdateSensitivityWeights + Update_Wj + + +Directives to save inversion results +==================================== + +.. autosummary:: + :toctree: generated/ + + SaveEveryIteration + SaveModelEveryIteration + SaveOutputDictEveryIteration + SaveOutputEveryIteration + + +Directives related to sparse inversions +======================================= + +.. autosummary:: + :toctree: generated/ + + Update_IRLS + + +Directives related to PGI +========================= + +.. autosummary:: + :toctree: generated/ + + PGI_AddMrefInSmooth + PGI_BetaAlphaSchedule + PGI_UpdateParameters + + +Directives related to joint inversions +====================================== + +.. autosummary:: + :toctree: generated/ + + SimilarityMeasureInversionDirective + SimilarityMeasureSaveOutputEveryIteration + PairedBetaEstimate_ByEig + PairedBetaSchedule + MovingAndMultiTargetStopping + + +Base directive classes +====================== +The ``InversionDirective`` class defines the basic class for all directives. +Inherit from this class when writing your own directive. The ``DirectiveList`` +is used under the hood to handle the execution of all directives passed to the +``SimPEG.inversion.BaseInversion``. + +.. autosummary:: + :toctree: generated/ + + InversionDirective + DirectiveList + +""" from .directives import ( InversionDirective, DirectiveList, diff --git a/docs/content/api/SimPEG.directives.rst b/docs/content/api/SimPEG.directives.rst new file mode 100644 index 0000000000..35999d49d0 --- /dev/null +++ b/docs/content/api/SimPEG.directives.rst @@ -0,0 +1 @@ +.. automodule:: SimPEG.directives diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index 55faddc116..8ffe60ffa9 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -32,6 +32,13 @@ Regularizations SimPEG.regularization +Directives +---------- +.. toctree:: + :maxdepth: 2 + + SimPEG.directives + Utilities --------- From d44a1e6fdb5771e56e3670e8cbcb8f6017bc56d4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 28 Mar 2024 13:02:20 -0700 Subject: [PATCH 383/455] Remove deprecated modelType in mag simulation (#1399) Remove the deprecated `modelType` property in the magnetic simulation and add test function checking that error is raised when trying to access it. Part of the solution to #1302 --- SimPEG/potential_fields/magnetics/simulation.py | 2 +- tests/pf/test_forward_Mag_Linear.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/SimPEG/potential_fields/magnetics/simulation.py index 11fdfb5a70..f90145000c 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/SimPEG/potential_fields/magnetics/simulation.py @@ -122,7 +122,7 @@ def G(self): return self._G modelType = deprecate_property( - model_type, "modelType", "model_type", removal_version="0.18.0" + model_type, "modelType", "model_type", removal_version="0.18.0", error=True ) @property diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index c3125b6499..9525edd4a1 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,3 +1,4 @@ +import pytest import unittest import discretize @@ -497,5 +498,20 @@ def get_block_inds(grid, block): np.testing.assert_allclose(data, d_amp) +def test_removed_modeltype(): + """Test if accesing removed modelType property raises error.""" + h = [[(2, 2)], [(2, 2)], [(2, 2)]] + mesh = discretize.TensorMesh(h) + receiver_location = np.array([[0, 0, 100]]) + receiver = mag.Point(receiver_location, components="tmi") + background_field = mag.UniformBackgroundField(receiver_list=[receiver]) + survey = mag.Survey(background_field) + mapping = maps.IdentityMap(mesh, nP=mesh.n_cells) + sim = mag.Simulation3DIntegral(mesh, survey=survey, chiMap=mapping) + message = "modelType has been removed, please use model_type." + with pytest.raises(NotImplementedError, match=message): + sim.modelType + + if __name__ == "__main__": unittest.main() From 7eaf970608025e6b3830d7c8340e16505d42fb1a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 28 Mar 2024 15:46:12 -0700 Subject: [PATCH 384/455] Remove mref property of PGI regularization (#1400) Remove the deprecated `mref` property in the PGI regularization class and add test function checking that error is raised when trying to access it. Part of the solution to #1302 --- SimPEG/regularization/pgi.py | 3 +-- .../regularizations/test_pgi_regularization.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 2c98c321f8..0a7a371f96 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -1393,6 +1393,5 @@ def reference_model(self, values: np.ndarray | float): "mref", "reference_model", "0.19.0", - future_warn=True, - error=False, + error=True, ) diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index 440e50a494..cc0ce5ac94 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -1,3 +1,4 @@ +import pytest import unittest import discretize @@ -469,5 +470,19 @@ def test_spherical_covariances(self): plt.show() +def test_removed_mref(): + """Test if PGI raises error when accessing removed mref property.""" + h = [[(2, 2)], [(2, 2)], [(2, 2)]] + mesh = discretize.TensorMesh(h) + n_components = 1 + gmm = WeightedGaussianMixture(mesh=mesh, n_components=n_components) + samples = np.random.default_rng(seed=42).normal(size=(mesh.n_cells, 2)) + gmm.fit(samples) + pgi = regularization.PGI(mesh=mesh, gmmref=gmm) + message = "mref has been removed, please use reference_model." + with pytest.raises(NotImplementedError, match=message): + pgi.mref + + if __name__ == "__main__": unittest.main() From 489851f1d8454b85b7f71c0bbe68ec0bbb006799 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 2 Apr 2024 09:48:37 -0700 Subject: [PATCH 385/455] Add link to User Tutorials to navbar in docs (#1401) Add an external link to the new User Tutorials in the navbar of the SimPEG documentation pages. --- docs/conf.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 25936ff517..88178fa245 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -231,6 +231,12 @@ def linkcode_resolve(domain, info): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. +external_links = [ + dict(name="User Tutorials", url="https://simpeg.xyz/user-tutorials"), + dict(name="SimPEG", url="https://simpeg.xyz"), + dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), +] + try: import pydata_sphinx_theme @@ -240,13 +246,7 @@ def linkcode_resolve(domain, info): html_use_modindex = True html_theme_options = { - "external_links": [ - {"name": "SimPEG", "url": "https://simpeg.xyz"}, - { - "name": "Contact", - "url": "https://mattermost.softwareunderground.org/simpeg", - }, - ], + "external_links": external_links, "icon_links": [ { "name": "GitHub", From eeb406f0a8341c6e250588afaa5a3b34d6e845a5 Mon Sep 17 00:00:00 2001 From: "Williams A. Lima" Date: Tue, 2 Apr 2024 19:13:10 -0300 Subject: [PATCH 386/455] Improve documentation for base simulation classes (#1295) Extend documentation for base simulation classes and in `SyntheticData` class. Fix typo and rst style in `mesh_utils.py`. Add `pymatsolver` to the list of `intersphinx_mapping` in Sphinx's `docs/conf.py`. --------- Co-authored-by: Devin C. Cowan Co-authored-by: Santiago Soler Co-authored-by: Lindsey Heagy --- SimPEG/data.py | 12 +- SimPEG/simulation.py | 639 +++++++++++++++++++++++++++++-------- SimPEG/utils/mesh_utils.py | 4 +- docs/conf.py | 1 + 4 files changed, 511 insertions(+), 145 deletions(-) diff --git a/SimPEG/data.py b/SimPEG/data.py index 4ba5ca3571..fa42a6d59e 100644 --- a/SimPEG/data.py +++ b/SimPEG/data.py @@ -115,7 +115,7 @@ def dobs(self): numpy.ndarray Notes - -------- + ----- This array can also be modified by directly indexing the data object using the a tuple of the survey's sources and receivers. @@ -363,8 +363,10 @@ def fromvec(self, v): class SyntheticData(Data): - r""" - Class for creating synthetic data. + r"""Synthetic data class. + + The ``SyntheticData`` class is a :py:class:`SimPEG.data.Data` class that allows the + user to keep track of both clean and noisy data. Parameters ---------- @@ -375,12 +377,12 @@ class SyntheticData(Data): Observed data. dclean : (nD) numpy.ndarray Noiseless data. - relative_error : SimPEG.data.UncertaintyArray + relative_error : float or np.ndarray Assign relative uncertainties to the data using relative error; sometimes referred to as percent uncertainties. For each datum, we assume the standard deviation of Gaussian noise is the relative error times the absolute value of the datum; i.e. :math:`C_{err} \times |d|`. - noise_floor : UncertaintyArray + noise_floor : float or np.ndarray Assign floor/absolute uncertainties to the data. For each datum, we assume standard deviation of Gaussian noise is equal to *noise_floor*. """ diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index da0c94b1cb..c857aa841e 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -1,5 +1,5 @@ """ -Define simulation classes +Define simulation classes. """ import os import inspect @@ -41,10 +41,39 @@ class BaseSimulation(props.HasModel): + r"""Base class for all geophysical forward simulations in SimPEG. + + The ``BaseSimulation`` class defines properties and methods inherited by + practical simulation classes in SimPEG. + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own simulation class. + + Parameters + ---------- + mesh : discretize.base.BaseMesh, optional + Mesh on which the forward problem is discretized. + survey : SimPEG.survey.BaseSurvey, optional + The survey for the simulation. + solver : None or pymatsolver.base.Base, optional + Numerical solver used to solve the forward problem. If ``None``, + an appropriate solver specific to the simulation class is set by default. + solver_opts : dict, optional + Solver-specific parameters. If ``None``, default parameters are used for + the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate + pairs of keyword arguments and parameter values for the solver. Please visit + `pymatsolver `__ to learn more + about solvers and their parameters. + sensitivity_path : str, optional + Path to directory where sensitivity file is stored. + counter : None or SimPEG.utils.Counter + SimPEG ``Counter`` object to store iterations and run-times. + verbose : bool, optional + Verbose progress printout. """ - BaseSimulation is the base class for all geophysical forward simulations in - SimPEG. - """ + + _REGISTRY = {} def __init__( self, @@ -73,18 +102,16 @@ def __init__( super().__init__(**kwargs) - ########################################################################### - # Properties - - _REGISTRY = {} - @property def mesh(self): - """Discretize mesh for the simulation + """Mesh for the simulation. + + For more on meshes, visit :py:class:`discretize.base.BaseMesh`. Returns ------- discretize.base.BaseMesh + Mesh on which the forward problem is discretized. """ return self._mesh @@ -101,6 +128,7 @@ def survey(self): Returns ------- SimPEG.survey.BaseSurvey + The survey for the simulation. """ return self._survey @@ -112,11 +140,12 @@ def survey(self, value): @property def counter(self): - """The counter. + """SimPEG ``Counter`` object to store iterations and run-times. Returns ------- None or SimPEG.utils.Counter + SimPEG ``Counter`` object to store iterations and run-times. """ return self._counter @@ -128,11 +157,12 @@ def counter(self, value): @property def sensitivity_path(self): - """Path to store the sensitivity. + """Path to directory where sensitivity file is stored. Returns ------- str + Path to directory where sensitivity file is stored. """ return self._sensitivity_path @@ -142,13 +172,25 @@ def sensitivity_path(self, value): @property def solver(self): - """Linear algebra solver (e.g. from pymatsolver). + r"""Numerical solver used in the forward simulation. + + Many forward simulations in SimPEG require solutions to discrete linear + systems of the form: + + .. math:: + \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} + + where :math:`\mathbf{A}` is an invertible matrix that depends on the + model :math:`\mathbf{m}`. The numerical solver can be set using the + ``solver`` property. In SimPEG, the + `pymatsolver `__ package + is used to create solver objects. Parameters specific to each solver + can be set manually using the ``solver_opts`` property. Returns ------- - class - A solver class that, when instantiated allows a multiplication with the - returned object. + pymatsolver.base.Base + Numerical solver used to solve the forward problem. """ return self._solver @@ -163,12 +205,18 @@ def solver(self, cls): @property def solver_opts(self): - """Options passed to the `solver` class on initialization. + """Solver-specific parameters. + + The parameters specific to the solver set with the ``solver`` property are set + upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. + This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments + and parameter values. Please visit `pymatsolver `__ + to learn more about solvers and their parameters. Returns ------- dict - Passed as keyword arguments to the solver. + keyword arguments and parameters passed to the solver. """ return self._solver_opts @@ -178,11 +226,12 @@ def solver_opts(self, value): @property def verbose(self): - """Verbosity flag. + """Verbose progress printout. Returns ------- bool + Verbose progress printout status. """ return self._verbose @@ -190,31 +239,37 @@ def verbose(self): def verbose(self, value): self._verbose = validate_type("verbose", value, bool) - ########################################################################### - # Methods - def fields(self, m=None): - """ - u = fields(m) - The field given the model. - :param numpy.ndarray m: model - :rtype: numpy.ndarray - :return: u, the fields + r"""Return the computed geophysical fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + + Returns + ------- + SimPEG.fields.Fields + Computed geophysical fields for the model provided. + """ raise NotImplementedError("fields has not been implemented for this ") def dpred(self, m=None, f=None): - r""" - dpred(m, f=None) - Create the projected data from a model. - The fields, f, (if provided) will be used for the predicted data - instead of recalculating the fields (which may be expensive!). + r"""Predicted data for the model provided. - .. math:: - - d_\text{pred} = P(f(m)) + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : SimPEG.fields.Fields, optional + If provided, will be used to compute the predicted data + without recalculating the fields. - Where P is a projection of the fields onto the data space. + Returns + ------- + (n_data, ) numpy.ndarray + The predicted data vector. """ if self.survey is None: raise AttributeError( @@ -237,51 +292,139 @@ def dpred(self, m=None, f=None): @timeIt def Jvec(self, m, v, f=None): - """ - Jv = Jvec(m, v, f=None) - Effect of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: Jv + r"""Compute the Jacobian times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an (n_data, n_param) matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jvec`` method computes the matrix-vector product + + .. math:: + \mathbf{u} = \mathbf{J \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_param, ) numpy.ndarray + Vector we are multiplying. + f : SimPEG.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jvec`. + + Returns + ------- + (n_data, ) numpy.ndarray + The Jacobian times a vector for the model and vector provided. """ raise NotImplementedError("Jvec is not yet implemented.") @timeIt def Jtvec(self, m, v, f=None): + r"""Compute the Jacobian transpose times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jtvec`` method computes the matrix-vector product with the adjoint-sensitivity + + .. math:: + \mathbf{u} = \mathbf{J^T \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : SimPEG.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + The Jacobian transpose times a vector for the model and vector provided. """ - Jtv = Jtvec(m, v, f=None) - Effect of transpose of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: JTv - """ - raise NotImplementedError("Jt is not yet implemented.") + raise NotImplementedError("Jtvec is not yet implemented.") @timeIt def Jvec_approx(self, m, v, f=None): - """Jvec_approx(m, v, f=None) - Approximate effect of J(m) on a vector v - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: approxJv + r"""Approximation of the Jacobian times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jvec_approx`` method **approximates** + the matrix-vector product: + + .. math:: + \mathbf{u} = \mathbf{J \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : SimPEG.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + Approximation of the Jacobian times a vector for the model provided. """ return self.Jvec(m, v, f) @timeIt def Jtvec_approx(self, m, v, f=None): - """Jtvec_approx(m, v, f=None) - Approximate effect of transpose of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: JTv + r"""Approximation of the Jacobian transpose times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jtvec_approx`` method **approximates** + the matrix-vector product: + + .. math:: + \mathbf{u} = \mathbf{J^T \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : SimPEG.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + Approximation of the Jacobian transpose times a vector for the model provided. """ return self.Jtvec(m, v, f) @@ -289,14 +432,27 @@ def Jtvec_approx(self, m, v, f=None): def residual(self, m, dobs, f=None): r"""The data residual. + This method computes and returns the data residual for the model provided. + Where :math:`\mathbf{d}_\text{obs}` are the observed data values, and :math:`\mathbf{d}_\text{pred}` + are the predicted data values for model parameters :math:`\mathbf{m}`, the data + residual is given by: + .. math:: + \mathbf{r}(\mathbf{m}) = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} - \mu_\\text{data} = \mathbf{d}_\\text{pred} - \mathbf{d}_\\text{obs} + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + dobs : (n_data, ) numpy.ndarray + The observed data values. + f : SimPEG.fields.Fields, optional + If provided, fields will not need to be recomputed when solving the forward problem. - :param numpy.ndarray m: geophysical model - :param numpy.ndarray f: fields - :rtype: numpy.ndarray - :return: data residual + Returns + ------- + (n_data, ) numpy.ndarray + The data residual. """ return mkvc(self.dpred(m, f=f) - dobs) @@ -311,27 +467,35 @@ def make_synthetic_data( random_seed=None, **kwargs, ): - """ - Make synthetic data given a model, and a standard deviation. + r"""Make synthetic data for the model and Gaussian noise provided. + + This method generates and returns a :py:class:`SimPEG.data.SyntheticData` object + for the model and standard deviation of Gaussian noise provided. Parameters ---------- - m : array - Array containing with geophysical model. - relative_error : float - Standard deviation. - noise_floor : float - Noise floor. - f : array or None - Fields for the given model (if pre-calculated). + m : (n_param, ) numpy.ndarray + The model parameters. + relative_error : float, numpy.ndarray + Assign relative uncertainties to the data using relative error; sometimes + referred to as percent uncertainties. For each datum, we assume the + standard deviation of Gaussian noise is the relative error times the + absolute value of the datum; i.e. :math:`C_\text{err} \times |d|`. + noise_floor : float, numpy.ndarray + Assign floor/absolute uncertainties to the data. For each datum, we assume + standard deviation of Gaussian noise is equal to `noise_floor`. + f : SimPEG.fields.Fields, optional + If provided, fields will not need to be recomputed when solving the + forward problem to obtain noiseless data. add_noise : bool Whether to add gaussian noise to the synthetic data or not. - random_seed : int or None - Random seed to pass to `numpy.random.default_rng`. + random_seed : int, optional + Random seed to pass to :py:class:`numpy.random.default_rng`. Returns ------- - SyntheticData + SimPEG.data.SyntheticData + A SimPEG synthetic data object, which organizes both clean and noisy data. """ std = kwargs.pop("std", None) @@ -363,30 +527,78 @@ def make_synthetic_data( class BaseTimeSimulation(BaseSimulation): - """ - Base class for a time domain simulation + r"""Base class for time domain simulations. + + The ``BaseTimeSimulation`` defines properties and methods that are required + when the finite volume approach is used to solve time-dependent forward simulations. + Presently, SimPEG discretizes in time using the backward Euler approach. + And as such, the user must now define the step lengths for the forward simulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh, optional + Mesh on which the forward problem is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + t0 : float, optional + Initial time, in seconds, for the time-dependent forward simulation. + time_steps : (n_steps, ) numpy.ndarray, optional + The time step lengths, in seconds, for the time domain simulation. + This property can be also be set using a compact form; see *Notes*. + + Notes + ----- + There are two ways in which the user can set the ``time_steps`` property + for the forward simulation. The most basic approach is to use a ``(n_steps, )`` + :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. + I.e.: + + >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] + + We can define also define the step lengths in compact for when the same + step length is reused multiple times in succession. In this case, the + ``time_steps`` property is set using a ``list`` of ``tuple``. Each + ``tuple`` contains the step length and number of times that step is repeated. + The time stepping defined above can be set equivalently with: + + >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] + + When set, the :py:func:`discretize.utils.unpack_widths` utility is + used to convert the ``list`` of ``tuple`` to its (n_steps, ) :py:class:`numpy.ndarray` + representation. """ + def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): + self.t0 = t0 + self.time_steps = time_steps + super().__init__(mesh=mesh, **kwargs) + @property def time_steps(self): - """The time steps for the time domain simulation. + """Time step lengths, in seconds, for the time domain simulation. + + There are two ways in which the user can set the ``time_steps`` property + for the forward simulation. The most basic approach is to use a ``(n_steps, )`` + :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. + I.e.: - You can set as an array of dt's or as a list of tuples/floats. - If it is set as a list, tuples are unpacked with - `discretize.utils.unpack_widths``. + >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] - For example, the following setters are the same:: + We can define also define the step lengths in compact for when the same + step length is reused multiple times in succession. In this case, the + ``time_steps`` property is set using a ``list`` of ``tuple``. Each + ``tuple`` contains the step length and number of times that step is repeated. + The time stepping defined above can be set equivalently with: - >>> sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] - >>> sim.time_steps = np.r_[1e-6,1e-6,1e-6,1e-5,1e-4,1e-4] + >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] + + When set, the :py:func:`discretize.utils.unpack_widths` utility is + used to convert the ``list`` of ``tuple`` to its ``(n_steps, )`` :py:class:`numpy.ndarray` + representation. Returns ------- - numpy.ndarray - - See Also - -------- - discretize.utils.unpack_widths + (n_steps, ) numpy.ndarray + The time step lengths for the time domain simulation. """ return self._time_steps @@ -401,11 +613,12 @@ def time_steps(self, value): @property def t0(self): - """Start time for the discretization. + """Initial time, in seconds, for the time-dependent forward simulation. Returns ------- float + Initial time, in seconds, for the time-dependent forward simulation. """ return self._t0 @@ -414,13 +627,21 @@ def t0(self, value): self._t0 = validate_float("t0", value) del self.time_mesh - def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): - self.t0 = t0 - self.time_steps = time_steps - super().__init__(mesh=mesh, **kwargs) - @property def time_mesh(self): + r"""Time mesh for easy interpolation to observation times. + + The time mesh is constructed internally from the :py:attr:`t0` and + :py:attr:`time_steps` properties using the :py:class:`discretize.TensorMesh` class. + The ``time_mesh`` property allows for easy interpolation from fields computed at + discrete time-steps, to an arbitrary set of observation + times within the continuous interval (:math:`t_0 , t_\text{end}`). + + Returns + ------- + discretize.TensorMesh + The time mesh. + """ if getattr(self, "_time_mesh", None) is None: self._time_mesh = TensorMesh( [ @@ -437,26 +658,32 @@ def time_mesh(self): @property def nT(self): + """Total number of time steps. + + Returns + ------- + int + Total number of time steps. + """ return self.time_mesh.n_cells @property def times(self): - "Modeling times" - return self.time_mesh.nodes_x + """Evaluation times. - def dpred(self, m=None, f=None): - r""" - dpred(m, f=None) - Create the projected data from a model. - The fields, f, (if provided) will be used for the predicted data - instead of recalculating the fields (which may be expensive!). - - .. math:: + Returns the discrete set of times at which the fields are computed for + the forward simulation. - d_\text{pred} = P(f(m)) - - Where P is a projection of the fields onto the data space. + Returns + ------- + (nT, ) numpy.ndarray + The discrete set of times at which the fields are computed for + the forward simulation. """ + return self.time_mesh.nodes_x + + def dpred(self, m=None, f=None): + # Docstring inherited from BaseSimulation. if self.survey is None: raise AttributeError( "The survey has not yet been set and is required to compute " @@ -482,16 +709,40 @@ def dpred(self, m=None, f=None): class LinearSimulation(BaseSimulation): - """ - Class for a linear simulation of the form + r"""Linear forward simulation class. + + The ``LinearSimulation`` class is used to define forward simulations of the form: .. math:: + \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) + + where :math:`\mathbf{m}` are the model parameters, :math:`\mathbf{f}` is a + mapping operator (optional) from the model space to a user-defined parameter space, + :math:`\mathbf{d}` is the predicted data vector, and :math:`\mathbf{G}` is an + ``(n_data, n_param)`` linear operator. + + The ``LinearSimulation`` class is generally used as a base class that is inherited by + other simulation classes within SimPEG. However, it can be used directly as a + simulation class if the :py:attr:`G` property is used to set the linear forward + operator directly. - d = Gm + By default, we assume the mapping operator :math:`\mathbf{f}` is the identity map, + and that the forward simulation reduces to: - where :math:`d` is a vector of the data, `G` is the simulation matrix and - :math:`m` is the model. - Inherit this class to build a linear simulation. + .. math:: + \mathbf{d} = \mathbf{G \, m} + + Parameters + ---------- + mesh : discretize.BaseMesh, optional + Mesh on which the forward problem is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + model_map : SimPEG.maps.BaseMap + Mapping from the model parameters to vector that the linear operator acts on. + G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx + The linear operator. For a ``model_map`` that maps within the same vector space + (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. + If not, the dimension ``n_param`` of the linear operator will depend on the mapping. """ linear_model, model_map, model_deriv = props.Invertible( @@ -516,6 +767,15 @@ def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwarg @property def G(self): + """The linear operator. + + Returns + ------- + (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrix + The linear operator. For a :py:attr:`model_map` that maps within the same vector space + (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. + If not, the dimension ``n_param`` of the linear operator will depend on the mapping. + """ if getattr(self, "_G", None) is not None: return self._G else: @@ -524,15 +784,17 @@ def G(self): @G.setter def G(self, G): - # Allows setting G in a LinearSimulation + # Allows setting G in a LinearSimulation. # TODO should be validated self._G = G def fields(self, m): + # Docstring inherited from BaseSimulation. self.model = m return self.G.dot(self.linear_model) def dpred(self, m=None, f=None): + # Docstring inherited from BaseSimulation if m is not None: self.model = m if f is not None: @@ -540,24 +802,80 @@ def dpred(self, m=None, f=None): return self.fields(self.model) def getJ(self, m, f=None): + r"""Returns the full Jacobian. + + The general definition of the linear forward simulation is: + + .. math:: + \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) + + where :math:`\mathbf{f}` is a mapping operator (optional) from the model space + to a user-defined parameter space, and :math:`\mathbf{G}` is an (n_data, n_param) + linear operator. The ``getJ`` method forms and returns the full Jacobian: + + .. math:: + \mathbf{J}(\mathbf{m}) = \mathbf{G} \frac{\partial \mathbf{f}}{\partial \mathbf{m}} + + for the model :math:`\mathbf{m}` provided. When :math:`\mathbf{f}` is the identity map + (default), the Jacobian is no longer model-dependent and reduces to: + + .. math:: + \mathbf{J} = \mathbf{G} + + Parameters + ---------- + m : numpy.ndarray + The model vector. + f : None + Precomputed fields are not used to speed up the computation of the + Jacobian for linear problems. + + Returns + ------- + J : (n_data, n_param) numpy.ndarray + :math:`J = G\frac{\partial f}{\partial\mathbf{m}}`. + Where :math:`f` is :attr:`model_map`. + """ self.model = m # self.model_deriv is likely a sparse matrix # and G is possibly dense, thus we need to do.. return (self.model_deriv.T.dot(self.G.T)).T def Jvec(self, m, v, f=None): + # Docstring inherited from BaseSimulation self.model = m return self.G.dot(self.model_deriv * v) def Jtvec(self, m, v, f=None): + # Docstring inherited from BaseSimulation self.model = m return self.model_deriv.T * self.G.T.dot(v) class ExponentialSinusoidSimulation(LinearSimulation): - r""" + r"""Simulation class for exponentially decaying sinusoidal kernel functions. + This is the simulation class for the linear problem consisting of - exponentially decaying sinusoids. The kernel functions take the form: + exponentially decaying sinusoids. The entries of the linear operator + :math:`\mathbf{G}` are: + + .. math:: + + G_{ik} = \int_\Omega e^{p \, j_i \, x_k} \cos(\pi \, q \, j_i \, x_k) \, dx + + The model is defined on a 1D :py:class:`discretize.TensorMesh`, and :math:`x_k` + are the cell center locations. :math:`p \leq 0` defines the rate of exponential + decay of the kernel functions. :math:`q` defines the rate of oscillation of + the kernel functions. And :math:`j_i \in [j_0, ... , j_n]` controls the spread + of the kernel functions; the number of which is set using the ``n_kernels`` + property. + + .. tip:: + + For proper scaling, we advise defining the 1D tensor mesh to + discretize the interval [0, 1]. + + The kernel functions take the form: .. math:: @@ -571,6 +889,21 @@ class ExponentialSinusoidSimulation(LinearSimulation): d_j = \int g_j(x) m(x) dx to define our data. + + Parameters + ---------- + n_kernels : int + The number of kernel factors for the linear problem; i.e. the number of + :math:`j_i \in [j_0, ... , j_n]`. This sets the number of rows + in the linear forward operator. + p : float + Exponent specifying the decay (`p \leq 0`) or growth (`p \geq 0`) of the kernel. For decay, set :math:`p \leq 0`. + q : float + Rate of oscillation of the kernel. + j0 : float + Minimum value for the spread of the kernel factors. + jn : float + Maximum value for the spread of the kernel factors. """ def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): @@ -583,11 +916,17 @@ def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): @property def n_kernels(self): - """The number of kernels for the linear problem + r"""The number of kernel factors for the linear problem. + + Where :math:`j_0` represents the minimum value for the spread of + kernel factors and :math:`j_n` represents the maximum, ``n_kernels`` + defines the number of kernel factors :math:`j_i \in [j_0, ... , j_n]`. + This ultimately sets the number of rows in the linear forward operator. Returns ------- int + The number of kernel factors for the linear problem. """ return self._n_kernels @@ -602,6 +941,7 @@ def p(self): Returns ------- float + Rate of exponential decay of the kernel. """ return self._p @@ -611,11 +951,12 @@ def p(self, value): @property def q(self): - """rate of oscillation of the kernel. + """Rate of oscillation of the kernel. Returns ------- float + Rate of oscillation of the kernel. """ return self._q @@ -625,11 +966,12 @@ def q(self, value): @property def j0(self): - """Maximum value for :math:`j_k = j_0`. + """Minimum value for the spread of the kernel factors. Returns ------- float + Minimum value for the spread of the kernel factors. """ return self._j0 @@ -639,11 +981,12 @@ def j0(self, value): @property def jn(self): - """Maximum value for :math:`j_k = j_n`. + """Maximum value for the spread of the kernel factors. Returns ------- float + Maximum value for the spread of the kernel factors. """ return self._jn @@ -653,16 +996,32 @@ def jn(self, value): @property def jk(self): - """ - Parameters controlling the spread of kernel functions + """The set of kernel factors controlling the spread of the kernel functions. + + Returns + ------- + (n_kernels, ) numpy.ndarray + The set of kernel factors controlling the spread of the kernel functions. """ if getattr(self, "_jk", None) is None: self._jk = np.linspace(self.j0, self.jn, self.n_kernels) return self._jk def g(self, k): - """ - Kernel functions for the decaying oscillating exponential functions. + """Kernel functions evaluated for kernel factor :math:`j_k`. + + This method computes the row of the linear forward operator for + the kernel functions for kernel factor :math:`j_k`, given :math:`k` + + Parameters + ---------- + k : int + Kernel functions for kernel factor *k* + + Returns + ------- + (n_param, ) numpy.ndarray + Kernel functions evaluated for kernel factor *k*. """ return np.exp(self.p * self.jk[k] * self.mesh.nodes_x) * np.cos( np.pi * self.q * self.jk[k] * self.mesh.nodes_x @@ -670,8 +1029,12 @@ def g(self, k): @property def G(self): - """ - Matrix whose rows are the kernel functions + """The linear forward operator. + + Returns + ------- + (n_kernels, n_param) numpy.ndarray + The linear forward operator. """ if getattr(self, "_G", None) is None: G_nodes = np.empty((self.mesh.n_nodes, self.n_kernels)) diff --git a/SimPEG/utils/mesh_utils.py b/SimPEG/utils/mesh_utils.py index 0d39f5c162..1fc3a8d580 100644 --- a/SimPEG/utils/mesh_utils.py +++ b/SimPEG/utils/mesh_utils.py @@ -11,8 +11,8 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): """Takes a triangulated surface and determine which mesh cells it intersects. - Paramters - --------- + Parameters + ---------- vrtx : (n_nodes, 3) numpy.ndarray of float The location of the vertices of the triangles trgl : (n_triang, 3) numpy.ndarray of int diff --git a/docs/conf.py b/docs/conf.py index 88178fa245..a47c5740e4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -423,6 +423,7 @@ def linkcode_resolve(domain, info): "matplotlib": ("https://matplotlib.org/stable/", None), "properties": ("https://propertiespy.readthedocs.io/en/latest/", None), "discretize": ("https://discretize.simpeg.xyz/en/main/", None), + "pymatsolver": ("https://pymatsolver.readthedocs.io/en/latest/", None), } numpydoc_xref_param_type = True From b8d8b8a0e6b2cd8d1511b6dbaf88973a9c5b1933 Mon Sep 17 00:00:00 2001 From: Ying Hu <64567062+YingHuuu@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:34:05 -0700 Subject: [PATCH 387/455] Enforce regularization `weights` as dictionaries (#1344) Only allow passing `weights` to regularizations as `None` or dictionaries. Raise error if `weights` are a different type. Remove lines that were assigning weights if the passed argument was an array. Update examples, tests and tutorials to follow the new interface. Update how the `PGI` constructor passes weights to the underlying `WeightedLeastSquares` objects. --------- Co-authored-by: Santiago Soler Co-authored-by: yinghu --- SimPEG/regularization/base.py | 18 +++++++++---- SimPEG/regularization/pgi.py | 3 ++- examples/01-maps/plot_sumMap.py | 6 +++-- examples/08-vrm/plot_inv_vrm_eq.py | 5 +++- ...1_PGI_Linear_1D_joint_WithRelationships.py | 8 +++--- .../regularizations/test_regularization.py | 25 +++++++++++-------- tests/em/vrm/test_vrminv.py | 7 +++++- 7 files changed, 47 insertions(+), 25 deletions(-) diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py index b82333025d..935efa9bba 100644 --- a/SimPEG/regularization/base.py +++ b/SimPEG/regularization/base.py @@ -65,6 +65,11 @@ def __init__( f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " f"Value of type {type(mesh)} provided." ) + if weights is not None and not isinstance(weights, dict): + raise TypeError( + f"Invalid 'weights' of type '{type(weights)}'. " + "It must be a dictionary with strings as keys and arrays as values." + ) # Raise errors on deprecated arguments: avoid old code that still uses # them to silently fail @@ -87,8 +92,6 @@ def __init__( self.reference_model = reference_model self.units = units if weights is not None: - if not isinstance(weights, dict): - weights = {"user_weights": weights} self.set_weights(**weights) @property @@ -608,7 +611,7 @@ class Smallness(BaseRegularization): or set after instantiation using the `set_weights` method: - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + >>> reg.set_weights(weights_1=array_1, weights_2=array_2) The default weights that account for cell dimensions in the regularization are accessed via: @@ -1603,6 +1606,13 @@ def __init__( else: self.length_scale_z = length_scale_z + # Check if weights is a dictionary, raise error if it's not + if weights is not None and not isinstance(weights, dict): + raise TypeError( + f"Invalid 'weights' of type '{type(weights)}'. " + "It must be a dictionary with strings as keys and arrays as values." + ) + # do this to allow child classes to also pass a list of objfcts to this constructor if "objfcts" not in kwargs: objfcts = [ @@ -1652,8 +1662,6 @@ def __init__( self.alpha_yy = alpha_yy self.alpha_zz = alpha_zz if weights is not None: - if not isinstance(weights, dict): - weights = {"user_weights": weights} self.set_weights(**weights) def set_weights(self, **weights): diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py index 0a7a371f96..24aeaf866d 100644 --- a/SimPEG/regularization/pgi.py +++ b/SimPEG/regularization/pgi.py @@ -1184,6 +1184,7 @@ def __init__( for model_map, wire, weights in zip( self.maplist, self.wiresmap.maps, weights_list ): + weights_i = {"pgi-weights": weights} if weights is not None else None objfcts += [ WeightedLeastSquares( alpha_s=0.0, @@ -1195,7 +1196,7 @@ def __init__( alpha_zz=alpha_zz, mesh=self.regularization_mesh, mapping=model_map * wire[1], - weights=weights, + weights=weights_i, **kwargs, ) ] diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index fe99b0cc2e..a07bb8712b 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -138,7 +138,8 @@ def run(plotIt=True): regMesh = TensorMesh([len(domains)]) reg_m1 = regularization.Sparse(regMesh, mapping=wires.homo) - reg_m1.set_weights(cell_weights=wires.homo * wr) + reg_m1.set_weights(weights=wires.homo * wr) + reg_m1.norms = [0, 2] reg_m1.reference_model = np.zeros(sumMap.shape[1]) @@ -146,7 +147,8 @@ def run(plotIt=True): reg_m2 = regularization.Sparse( mesh, active_cells=actv, mapping=wires.hetero, gradient_type="components" ) - reg_m2.set_weights(cell_weights=wires.hetero * wr) + reg_m2.set_weights(weights=wires.hetero * wr) + reg_m2.norms = [0, 0, 0, 0] reg_m2.reference_model = np.zeros(sumMap.shape[1]) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index e0eee4ff6f..dab9190535 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -196,7 +196,10 @@ w = w / np.max(w) w = w -reg = regularization.Smallness(mesh=mesh, active_cells=actCells, weights=w) +reg = regularization.Smallness( + mesh=mesh, active_cells=actCells, weights={"cell_weights": w} +) + opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 ) diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index adabf3f35f..afaa07b183 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -233,13 +233,13 @@ def g(k): # WeightedLeastSquares Inversion reg1 = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m1 + mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m1, weights={"cell_weights": wr1} ) -reg1.set_weights(cell_weights=wr1) + reg2 = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m2 + mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m2, weights={"cell_weights": wr2} ) -reg2.set_weights(cell_weights=wr2) + reg = reg1 + reg2 opt = optimization.ProjectedGNCG( diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 5fc7773ce6..779890667d 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -302,7 +302,8 @@ def test_mappings_and_cell_weights(self): wires = maps.Wires(("sigma", mesh.nC), ("mu", mesh.nC)) - reg = regularization.Smallness(mesh, mapping=wires.sigma, weights=cell_weights) + reg = regularization.Smallness(mesh, mapping=wires.sigma) + reg.set_weights(cell_weights=cell_weights) objfct = objective_function.L2ObjectiveFunction( W=utils.sdiag(np.sqrt(cell_weights * mesh.cell_volumes)), @@ -337,8 +338,7 @@ def test_update_of_sparse_norms(self): v = np.random.rand(mesh.nC) cell_weights = np.random.rand(mesh.nC) - - reg = regularization.Sparse(mesh, weights=cell_weights) + reg = regularization.Sparse(mesh, weights={"cell_weights": cell_weights}) np.testing.assert_equal(reg.norms, [1, 1, 1, 1]) @@ -725,14 +725,6 @@ def test_user_defined_weights_as_dict(self, mesh): reg = BaseRegularization(mesh, weights=weights) assert reg.weights_keys == ["dummy_weight"] - def test_user_defined_weights_as_array(self, mesh): - """ - Test weights_keys after user defined weights as dictionary - """ - weights = np.ones(mesh.n_cells) - reg = BaseRegularization(mesh, weights=weights) - assert reg.weights_keys == ["user_weights"] - @pytest.mark.parametrize( "regularization_class", (Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder) ) @@ -908,5 +900,16 @@ def test_removed_class(self, regularization_class): regularization_class() +@pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) +) +def test_invalid_weights_type(regularization_class): + """Test error after passing weights as invalid type.""" + mesh = discretize.TensorMesh([[(2, 2)]]) + msg = "Invalid 'weights' of type ''" + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, weights=np.array([1.0])) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 8511f0a704..b4f316c02f 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -69,7 +69,12 @@ def test_basic_inversion(self): ** 0.25 ) reg = regularization.WeightedLeastSquares( - meshObj, alpha_s=0.01, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, weights=W + meshObj, + alpha_s=0.01, + alpha_x=1.0, + alpha_y=1.0, + alpha_z=1.0, + weights={"weights": W}, ) opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 From d06feea77391017c56da179e14eede94c7ee71f4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 3 Apr 2024 11:16:33 -0700 Subject: [PATCH 388/455] Minor adjustments to Sphinx configuration (#1398) Align navbar elements to the left to avoid wrapping text in nav items. Remove plausible line from the html template and set it through sphinx pydata theme. --- docs/_templates/layout.html | 2 -- docs/conf.py | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index f208c5eced..d4fe319939 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -8,6 +8,4 @@ - - {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index a47c5740e4..1e1b4419d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -276,6 +276,11 @@ def linkcode_resolve(domain, info): ], "use_edit_page_button": False, "collapse_navigation": True, + "analytics": { + "plausible_analytics_domain": "docs.simpeg.xyz", + "plausible_analytics_url": "https://plausible.io/js/script.js", + }, + "navbar_align": "left", # make elements closer to logo on the left } html_logo = "images/simpeg-logo.png" From 9af04b197dbc2e82785e04430d8a7758ecea0719 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 3 Apr 2024 11:22:47 -0700 Subject: [PATCH 389/455] Update AUTHORS.rst (#1259) Add to the Authors: - Xiaolong Wei, @xiaolongw1223 - Santiago Soler, @santisoler - Nick Williams, @nwilliams-kobold - John Weis, @johnweis0480 - Kalen Martens, @kalen-sj --- AUTHORS.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 55de84f924..65518b241d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,4 +18,9 @@ - Thibaut Astic, (`@thast `_) - Michael Mitchell, (`@micmitch `_) - I-Kang Ding, (`@ikding `_) -- Richard Scott (`@bluetyson `_) +- Richard Scott, (`@bluetyson `_) +- Xiaolong Wei, (`@xiaolongw1223 `_) +- Santiago Soler, (`@santisoler `_) +- Nick Williams, (`@nwilliams-kobold `_) +- John Weis, (`@johnweis0480 `_) +- Kalen Martens, (`@kalen-sj `_) From 20d0379d7aaa037faecf92bf3156969498449487 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 3 Apr 2024 11:50:21 -0700 Subject: [PATCH 390/455] Update year in LICENSE (#1404) Update year in LICENSE file to 2024. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 79e70af749..cd67a7669e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2023 SimPEG Developers +Copyright (c) 2013-2024 SimPEG Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 9c3ff437c200c65350dfc8f6e0b99a323f4393e4 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 3 Apr 2024 15:51:20 -0600 Subject: [PATCH 391/455] Dask MetaSim (#1199) Adds the `dask` version of the `MetaSimulation`. Essentially it never explicitly sends around fields objects, and it avoids sending around simulations (after they have gone through the setter). Every operation that involves one of the internal simulations is explicitly done on the worker that owns that simulation. The `fields` returned by this class are actually `Future`s that live on the worker that owns the respective simulation. Most items that live on this class are `Future`s of the respective item in the `MetaSimulation` class ( e.g. `sim.mappings` are a list of `Future` maps instead of a `list` of maps). As a side effect, it is actually possible to initialize the class with lists of Futures. This is useful if you want to avoid creating everything on the main thread and then serializing and sending the result out to each worker, instead you can create a function which returns an object and send that function to each worker. --- SimPEG/meta/__init__.py | 9 +- SimPEG/meta/dask_sim.py | 644 +++++++++++++++++++++++++ SimPEG/meta/simulation.py | 19 +- tests/meta/test_dask_meta.py | 393 +++++++++++++++ tests/meta/test_meta_sim.py | 1 - tests/meta/test_multiprocessing_sim.py | 45 +- 6 files changed, 1085 insertions(+), 26 deletions(-) create mode 100644 SimPEG/meta/dask_sim.py create mode 100644 tests/meta/test_dask_meta.py diff --git a/SimPEG/meta/__init__.py b/SimPEG/meta/__init__.py index e60961e273..9dab54e5be 100644 --- a/SimPEG/meta/__init__.py +++ b/SimPEG/meta/__init__.py @@ -50,7 +50,12 @@ Dask ---- -Coming soon! +.. autosummary:: + :toctree: generated/ + + DaskMetaSimulation + DaskSumMetaSimulation + DaskRepeatedSimulation MPI --- @@ -69,3 +74,5 @@ MultiprocessingSumMetaSimulation, MultiprocessingRepeatedSimulation, ) + +from .dask_sim import DaskMetaSimulation, DaskSumMetaSimulation, DaskRepeatedSimulation diff --git a/SimPEG/meta/dask_sim.py b/SimPEG/meta/dask_sim.py new file mode 100644 index 0000000000..268f5260dc --- /dev/null +++ b/SimPEG/meta/dask_sim.py @@ -0,0 +1,644 @@ +import numpy as np + +from SimPEG.simulation import BaseSimulation +from SimPEG.survey import BaseSurvey +from SimPEG.maps import IdentityMap +from SimPEG.utils import validate_list_of_types, validate_type +from SimPEG.props import HasModel +import itertools +from dask.distributed import Client +from dask.distributed import Future +from .simulation import MetaSimulation, SumMetaSimulation +import scipy.sparse as sp +from operator import add +import warnings + + +def _store_model(mapping, sim, model): + sim.model = mapping * model + + +def _calc_fields(mapping, sim, model, apply_map=False): + if apply_map and model is not None: + return sim.fields(m=mapping @ model) + else: + return sim.fields(m=sim.model) + + +def _calc_dpred(mapping, sim, model, field, apply_map=False): + if apply_map and model is not None: + return sim.dpred(m=mapping @ model) + else: + return sim.dpred(m=sim.model, f=field) + + +def _j_vec_op(mapping, sim, model, field, v, apply_map=False): + sim_v = mapping.deriv(model) @ v + if apply_map: + return sim.Jvec(mapping @ model, sim_v, f=field) + else: + return sim.Jvec(sim.model, sim_v, f=field) + + +def _jt_vec_op(mapping, sim, model, field, v, apply_map=False): + if apply_map: + jtv = sim.Jtvec(mapping @ model, v, f=field) + else: + jtv = sim.Jtvec(sim.model, v, f=field) + return mapping.deriv(model).T @ jtv + + +def _get_jtj_diag(mapping, sim, model, field, w, apply_map=False): + w = sp.diags(w) + if apply_map: + jtj = sim.getJtJdiag(mapping @ model, w, f=field) + else: + jtj = sim.getJtJdiag(sim.model, w, f=field) + sim_jtj = sp.diags(np.sqrt(jtj)) + m_deriv = mapping.deriv(model) + return np.asarray((sim_jtj @ m_deriv).power(2).sum(axis=0)).flatten() + + +def _reduce(client, operation, items): + while len(items) > 1: + new_reduce = client.map(operation, items[::2], items[1::2]) + if len(items) % 2 == 1: + new_reduce[-1] = client.submit(operation, new_reduce[-1], items[-1]) + items = new_reduce + return client.gather(items[0]) + + +def _validate_type_or_future_of_type( + property_name, + objects, + obj_type, + client, + workers=None, + return_workers=False, +): + try: + # validate as a list of things that need to be sent. + objects = validate_list_of_types( + property_name, objects, obj_type, ensure_unique=True + ) + if workers is None: + objects = client.scatter(objects) + else: + tmp = [] + for obj, worker in zip(objects, workers): + tmp.append(client.scatter([obj], workers=worker)[0]) + objects = tmp + except TypeError: + pass + # ensure list of futures + objects = validate_list_of_types( + property_name, + objects, + Future, + ) + # Figure out where everything lives + who = client.who_has(objects) + if workers is None: + workers = [] + for obj in objects: + workers.append(who[obj.key]) + else: + # Issue a warning if the future is not on the expected worker + for i, (obj, worker) in enumerate(zip(objects, workers)): + obj_owner = client.who_has(obj)[obj.key] + if obj_owner != worker: + warnings.warn( + f"{property_name} {i} is not on the expected worker.", stacklevel=2 + ) + + # Ensure this runs on the expected worker + futures = [] + for obj, worker in zip(objects, workers): + futures.append( + client.submit(lambda v: not isinstance(v, obj_type), obj, workers=worker) + ) + is_not_obj = np.array(client.gather(futures)) + if np.any(is_not_obj): + raise TypeError(f"{property_name} futures must be an instance of {obj_type}") + + if return_workers: + return objects, workers + else: + return objects + + +class DaskMetaSimulation(MetaSimulation): + """Dask Distributed version of simulation of simulations. + + This class makes use of `dask.distributed` module to provide + concurrency, executing the internal simulations in parallel. This class + is meant to be a (mostly) drop in replacement for :class:`.MetaSimulation`. + If you want to test your implementation, we recommend starting with a + small problem using `MetaSimulation`, then switching it to this class. + the serial version of this class is good for testing correctness. + + Parameters + ---------- + simulations : (n_sim) list of SimPEG.simulation.BaseSimulation or list of dask.distributed.Future + The list of unique simulations (or futures that would return a simulation) + that each handle a piece of the problem. + mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future + The map for every simulation (or futures that would return a map). Every + map should accept the same length model, and output a model appropriate + for its paired simulation. + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + def __init__(self, simulations, mappings, client): + self._client = validate_type("client", client, Client, cast=False) + super().__init__(simulations, mappings) + + def _make_survey(self): + survey = BaseSurvey([]) + vnD = [] + client = self.client + for sim, worker in zip(self.simulations, self._workers): + vnD.append(client.submit(lambda s: s.survey.nD, sim, workers=worker)) + vnD = client.gather(vnD) + survey._vnD = vnD + return survey + + @property + def simulations(self): + """The future list of simulations. + + Returns + ------- + (n_sim) list of distributed.Future SimPEG.simulation.BaseSimulation + """ + return self._simulations + + @simulations.setter + def simulations(self, value): + client = self.client + simulations, workers = _validate_type_or_future_of_type( + "simulations", value, BaseSimulation, client, return_workers=True + ) + self._simulations = simulations + self._workers = workers + + @property + def mappings(self): + """The future mappings paired to each simulation. + + Every mapping should accept the same length model, and output + a model that is consistent with the simulation. + + Returns + ------- + (n_sim) list of distributed.Future SimPEG.maps.IdentityMap + """ + return self._mappings + + @mappings.setter + def mappings(self, value): + client = self.client + if self._repeat_sim: + mappings, workers = _validate_type_or_future_of_type( + "mappings", value, IdentityMap, client, return_workers=True + ) + else: + workers = self._workers + if len(value) != len(self.simulations): + raise ValueError( + "Must provide the same number of mappings and simulations." + ) + mappings = _validate_type_or_future_of_type( + "mappings", value, IdentityMap, client, workers=workers + ) + + # validate mapping shapes and simulation shapes + model_len = client.submit(lambda v: v.shape[1], mappings[0]).result() + + def check_mapping(mapping, sim, model_len): + if mapping.shape[1] != model_len: + # Bad mapping model length + return 1 + map_out_shape = mapping.shape[0] + for name in sim._act_map_names: + sim_mapping = getattr(sim, name) + sim_in_shape = sim_mapping.shape[1] + if ( + map_out_shape != "*" + and sim_in_shape != "*" + and sim_in_shape != map_out_shape + ): + # Inconsistent simulation input and mapping output + return 2 + # All good + return 0 + + error_checks = [] + for mapping, sim, worker in zip(mappings, self.simulations, workers): + # if it was a repeat sim, this should cause the simulation to be transfered + # to each worker. + error_checks.append( + client.submit(check_mapping, mapping, sim, model_len, workers=worker) + ) + error_checks = np.asarray(client.gather(error_checks)) + + if np.any(error_checks == 1): + raise ValueError("All mappings must have the same input length") + if np.any(error_checks == 2): + raise ValueError( + f"Simulations and mappings at indices {np.where(error_checks==2)}" + f" are inconsistent." + ) + + self._mappings = mappings + if self._repeat_sim: + self._workers = workers + + @property + def _model_map(self): + # create a bland mapping that has the correct input shape + # to test against model inputs, avoids pulling the first + # mapping back to the main task. + if not hasattr(self, "__model_map"): + client = self.client + n_m = client.submit( + lambda v: v.shape[1], + self.mappings[0], + workers=self._workers[0], + ) + n_m = client.gather(n_m) + self.__model_map = IdentityMap(nP=n_m) + return self.__model_map + + @property + def client(self): + """The distributed client that handles the internal tasks. + + Returns + ------- + distributed.Client + """ + return self._client + + @property + def model(self): + return self._model + + @model.setter + def model(self, value): + updated = HasModel.model.fset(self, value) + # Only send the model to the internal simulations if it was updated. + if updated: + client = self.client + [self._m_as_future] = client.scatter([self._model], broadcast=True) + if not self._repeat_sim: + futures = [] + for mapping, sim, worker in zip( + self.mappings, self.simulations, self._workers + ): + futures.append( + client.submit( + _store_model, + mapping, + sim, + self._m_as_future, + workers=worker, + ) + ) + self.client.gather( + futures + ) # blocking call to ensure all models were stored + + def fields(self, m): + self.model = m + client = self.client + m_future = self._m_as_future + # The above should pass the model to all the internal simulations. + f = [] + for mapping, sim, worker in zip(self.mappings, self.simulations, self._workers): + f.append( + client.submit( + _calc_fields, + mapping, + sim, + m_future, + self._repeat_sim, + workers=worker, + ) + ) + return f + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + client = self.client + m_future = self._m_as_future + dpred = [] + for mapping, sim, worker, field in zip( + self.mappings, self.simulations, self._workers, f + ): + dpred.append( + client.submit( + _calc_dpred, + mapping, + sim, + m_future, + field, + self._repeat_sim, + workers=worker, + ) + ) + return np.concatenate(client.gather(dpred)) + + def Jvec(self, m, v, f=None): + self.model = m + m_future = self._m_as_future + if f is None: + f = self.fields(m) + client = self.client + [v_future] = client.scatter([v], broadcast=True) + j_vec = [] + for mapping, sim, worker, field in zip( + self.mappings, self.simulations, self._workers, f + ): + j_vec.append( + client.submit( + _j_vec_op, + mapping, + sim, + m_future, + field, + v_future, + self._repeat_sim, + workers=worker, + ) + ) + return np.concatenate(self.client.gather(j_vec)) + + def Jtvec(self, m, v, f=None): + self.model = m + m_future = self._m_as_future + if f is None: + f = self.fields(m) + jt_vec = [] + client = self.client + for i, (mapping, sim, worker, field) in enumerate( + zip(self.mappings, self.simulations, self._workers, f) + ): + jt_vec.append( + client.submit( + _jt_vec_op, + mapping, + sim, + m_future, + field, + v[self._data_offsets[i] : self._data_offsets[i + 1]], + self._repeat_sim, + workers=worker, + ) + ) + # Do the sum by a reduction operation to avoid gathering a vector + # of size n_simulations by n_model parameters on the head. + return _reduce(client, add, jt_vec) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + m_future = self._m_as_future + if getattr(self, "_jtjdiag", None) is None: + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + jtj_diag = [] + client = self.client + if f is None: + f = self.fields(m) + for i, (mapping, sim, worker, field) in enumerate( + zip(self.mappings, self.simulations, self._workers, f) + ): + sim_w = W[self._data_offsets[i] : self._data_offsets[i + 1]] + jtj_diag.append( + client.submit( + _get_jtj_diag, + mapping, + sim, + m_future, + field, + sim_w, + self._repeat_sim, + workers=worker, + ) + ) + self._jtjdiag = _reduce(client, add, jtj_diag) + + return self._jtjdiag + + +class DaskSumMetaSimulation(DaskMetaSimulation, SumMetaSimulation): + """A dask distributed version of :class:`.SumMetaSimulation`. + + A meta simulation that sums the results of the many individual + simulations. + + Parameters + ---------- + simulations : (n_sim) list of SimPEG.simulation.BaseSimulation or list of dask.distributed.Future + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + def __init__(self, simulations, mappings, client): + super().__init__(simulations, mappings, client) + + def _make_survey(self): + survey = BaseSurvey([]) + client = self.client + n_d = client.submit(lambda s: s.survey.nD, self.simulations[0]).result() + survey._vnD = [ + n_d, + ] + return survey + + @DaskMetaSimulation.simulations.setter + def simulations(self, value): + client = self.client + simulations, workers = _validate_type_or_future_of_type( + "simulations", value, BaseSimulation, client, return_workers=True + ) + n_d = client.submit(lambda s: s.survey.nD, simulations[0], workers=workers[0]) + sim_check = [] + for sim, worker in zip(simulations, workers): + sim_check.append( + client.submit(lambda s, n: s.survey.nD != n, sim, n_d, workers=worker) + ) + if np.any(client.gather(sim_check)): + raise ValueError("All simulations must have the same number of data.") + self._simulations = simulations + self._workers = workers + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + client = self.client + dpred = [] + for sim, worker, field in zip(self.simulations, self._workers, f): + dpred.append( + client.submit(_calc_dpred, None, sim, None, field, workers=worker) + ) + return _reduce(client, add, dpred) + + def Jvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + client = self.client + [v_future] = client.scatter([v], broadcast=True) + j_vec = [] + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + j_vec.append( + client.submit( + _j_vec_op, + mapping, + sim, + self._m_as_future, + field, + v_future, + workers=worker, + ) + ) + return _reduce(client, add, j_vec) + + def Jtvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + jt_vec = [] + client = self.client + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + jt_vec.append( + client.submit( + _jt_vec_op, + mapping, + sim, + self._m_as_future, + field, + v, + workers=worker, + ) + ) + # Do the sum by a reduction operation to avoid gathering a vector + # of size n_simulations by n_model parameters on the head. + return _reduce(client, add, jt_vec) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + jtj_diag = [] + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + client = self.client + if f is None: + f = self.fields(m) + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + jtj_diag.append( + client.submit( + _get_jtj_diag, + mapping, + sim, + self._m_as_future, + field, + W, + workers=worker, + ) + ) + self._jtjdiag = _reduce(client, add, jtj_diag) + + return self._jtjdiag + + +class DaskRepeatedSimulation(DaskMetaSimulation): + """A multiprocessing version of the :class:`.RepeatedSimulation`. + + This class makes use of a single simulation that is copied to each internal + process, but only once per process. + + This simulation shares internals with the :class:`.MultiprocessingMetaSimulation`. + class, as such please see that documentation for details regarding how to properly + use multiprocessing on your operating system. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseSimulation or dask.distributed.Future + The simulation to use repeatedly with different mappings. + mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future + The list of different mappings to use (or futures that each return a mapping). + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + _repeat_sim = True + + def __init__(self, simulation, mappings, client): + self._client = validate_type("client", client, Client, cast=False) + + self.simulation = simulation + self.mappings = mappings + + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): + survey = BaseSurvey([]) + nD = self.client.submit(lambda s: s.survey.nD, self.simulation).result() + survey._vnD = len(self.mappings) * [nD] + return survey + + @property + def simulations(self): + return itertools.repeat(self.simulation) + + @property + def simulation(self): + """The internal simulation. + + Returns + ------- + distributed.Future of SimPEG.simulation.BaseSimulation + """ + return self._simulation + + @simulation.setter + def simulation(self, value): + client = self.client + if isinstance(value, BaseSimulation): + # Scatter sim to every client + [ + value, + ] = client.scatter([value], broadcast=True) + if not ( + isinstance(value, Future) + and client.submit(lambda s: isinstance(s, BaseSimulation), value).result() + ): + raise TypeError( + "simulation must be an instance of BaseSimulation or a Future that returns" + " a BaseSimulation" + ) + self._simulation = value diff --git a/SimPEG/meta/simulation.py b/SimPEG/meta/simulation.py index a2327b9606..aa62a05b0b 100644 --- a/SimPEG/meta/simulation.py +++ b/SimPEG/meta/simulation.py @@ -98,11 +98,14 @@ def __init__(self, simulations, mappings): self.model = None # give myself a BaseSurvey that has the number of data equal # to the sum of the sims' data. + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): survey = BaseSurvey([]) vnD = [sim.survey.nD for sim in self.simulations] survey._vnD = vnD - self.survey = survey - self._data_offsets = np.cumsum(np.r_[0, vnD]) + return survey @property def simulations(self): @@ -352,11 +355,14 @@ def __init__(self, simulations, mappings): self.mappings = mappings self.model = None # give myself a BaseSurvey + self.survey = self._make_survey() + + def _make_survey(self): survey = BaseSurvey([]) survey._vnD = [ self.simulations[0].survey.nD, ] - self.survey = survey + return survey @MetaSimulation.simulations.setter def simulations(self, value): @@ -442,11 +448,14 @@ def __init__(self, simulation, mappings): self.simulation = simulation self.mappings = mappings self.model = None + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): survey = BaseSurvey([]) vnD = len(self.mappings) * [self.simulation.survey.nD] survey._vnD = vnD - self.survey = survey - self._data_offsets = np.cumsum(np.r_[0, vnD]) + return survey @property def simulations(self): diff --git a/tests/meta/test_dask_meta.py b/tests/meta/test_dask_meta.py new file mode 100644 index 0000000000..ac44d70a1a --- /dev/null +++ b/tests/meta/test_dask_meta.py @@ -0,0 +1,393 @@ +import numpy as np +from SimPEG.potential_fields import gravity +from SimPEG.electromagnetics.static import resistivity as dc +from SimPEG import maps +from discretize import TensorMesh +import scipy.sparse as sp +import pytest + +from SimPEG.meta import ( + MetaSimulation, + SumMetaSimulation, + RepeatedSimulation, + DaskMetaSimulation, + DaskSumMetaSimulation, + DaskRepeatedSimulation, +) + +from distributed import Client, LocalCluster + + +@pytest.fixture(scope="module") +def cluster(): + dask_cluster = LocalCluster( + n_workers=2, threads_per_worker=2, dashboard_address=None, processes=True + ) + yield dask_cluster + dask_cluster.close() + + +def test_meta_correctness(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + # split by chunks of sources + chunk_size = 3 + sims = [] + mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + sims.append( + dc.Simulation3DNodal( + mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap() + ) + ) + mappings.append(maps.IdentityMap()) + + serial_sim = MetaSimulation(sims, mappings) + dask_sim = DaskMetaSimulation(sims, mappings, client) + + # test fields objects + f_meta = serial_sim.fields(m_test) + f_dask = dask_sim.fields(m_test) + # Can't serialize DC nodal fields here, so can't directly test them. + # sol_meta = np.concatenate([f[:, "phiSolution"] for f in f_meta], axis=1) + # sol_dask = np.concatenate([f.result()[:, "phiSolution"] for f in f_dask], axis=1) + # np.testing.assert_allclose(sol_meta, sol_dask) + + # test data output + d_meta = serial_sim.dpred(m_test, f=f_meta) + d_dask = dask_sim.dpred(m_test, f=f_dask) + np.testing.assert_allclose(d_dask, d_meta) + + # test Jvec + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) + jvec_meta = serial_sim.Jvec(m_test, u, f=f_meta) + jvec_dask = dask_sim.Jvec(m_test, u, f=f_dask) + + np.testing.assert_allclose(jvec_dask, jvec_meta) + + # test Jtvec + v = rng.random(serial_sim.survey.nD) + jtvec_meta = serial_sim.Jtvec(m_test, v, f=f_meta) + jtvec_dask = dask_sim.Jtvec(m_test, v, f=f_dask) + + np.testing.assert_allclose(jtvec_dask, jtvec_meta) + + # test get diag + diag_meta = serial_sim.getJtJdiag(m_test, f=f_meta) + diag_dask = dask_sim.getJtJdiag(m_test, f=f_dask) + + np.testing.assert_allclose(diag_dask, diag_meta) + + # test things also works without passing optional fields + dask_sim.model = m_test + d_dask2 = dask_sim.dpred() + np.testing.assert_allclose(d_dask, d_dask2) + + jvec_dask2 = dask_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_dask, jvec_dask2) + + jtvec_dask2 = dask_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_dask, jtvec_dask2) + + # also pass a diagonal matrix here for testing. + dask_sim._jtjdiag = None + W = sp.eye(dask_sim.survey.nD) + diag_dask2 = dask_sim.getJtJdiag(m_test, W=W) + np.testing.assert_allclose(diag_dask, diag_dask2) + + +def test_sum_sim_correctness(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + # Create gravity sum sims + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) + + g_mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + g_sims = [ + gravity.Simulation3DIntegral( + mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + gravity.Simulation3DIntegral( + mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + ] + + serial_sim = SumMetaSimulation(g_sims, g_mappings) + parallel_sim = DaskSumMetaSimulation(g_sims, g_mappings, client) + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + # test fields objects + f_full = serial_sim.fields(m_test) + f_meta = parallel_sim.fields(m_test) + # Again don't serialize and collect the fields on the main + # process directly. + # np.testing.assert_allclose(f_full, sum(f_meta)) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_full) + d_meta = parallel_sim.dpred(m_test, f=f_meta) + np.testing.assert_allclose(d_full, d_meta, rtol=1e-6) + + rng = np.random.default_rng(0) + + # test Jvec + u = rng.random(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_full) + jvec_meta = parallel_sim.Jvec(m_test, u, f=f_meta) + + np.testing.assert_allclose(jvec_full, jvec_meta, rtol=1e-6) + + # test Jtvec + v = rng.random(survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_full) + jtvec_meta = parallel_sim.Jtvec(m_test, v, f=f_meta) + + np.testing.assert_allclose(jtvec_full, jtvec_meta, rtol=1e-6) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_full) + diag_meta = parallel_sim.getJtJdiag(m_test, f=f_meta) + + np.testing.assert_allclose(diag_full, diag_meta, rtol=1e-6) + + # test things also works without passing optional kwargs + parallel_sim.model = m_test + d_meta2 = parallel_sim.dpred() + np.testing.assert_allclose(d_meta, d_meta2) + + jvec_meta2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_meta, jvec_meta2) + + jtvec_meta2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_meta, jtvec_meta2) + + parallel_sim._jtjdiag = None + diag_meta2 = parallel_sim.getJtJdiag(m_test) + np.testing.assert_allclose(diag_meta, diag_meta2) + + +def test_repeat_sim_correctness(cluster): + with Client(cluster) as client: + # meta sim is tested for correctness + # so can test the repeat against the meta sim + mesh = TensorMesh([8, 8, 8], origin="CCN") + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + grav_sim = gravity.Simulation3DIntegral( + mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ) + + time_mesh = TensorMesh([8], origin=[0]) + sim_ts = np.linspace(0, 1, 6) + + repeat_mappings = [] + eye = sp.eye(mesh.n_cells, mesh.n_cells) + for t in sim_ts: + ave_time = time_mesh.get_interpolation_matrix([t]) + ave_full = sp.kron(ave_time, eye, format="csr") + repeat_mappings.append(maps.LinearMap(ave_full)) + + serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) + parallel_sim = DaskRepeatedSimulation(grav_sim, repeat_mappings, client) + + rng = np.random.default_rng(0) + model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) + + # test field things + f_full = serial_sim.fields(model) + f_meta = parallel_sim.fields(model) + # np.testing.assert_equal(np.c_[f_full], np.c_[f_meta]) + + d_full = serial_sim.dpred(model, f_full) + d_repeat = parallel_sim.dpred(model, f_meta) + np.testing.assert_allclose(d_full, d_repeat, rtol=1e-6) + + # test Jvec + u = rng.random(len(model)) + jvec_full = serial_sim.Jvec(model, u, f=f_full) + jvec_meta = parallel_sim.Jvec(model, u, f=f_meta) + np.testing.assert_allclose(jvec_full, jvec_meta, rtol=1e-6) + + # test Jtvec + v = rng.random(len(sim_ts) * survey.nD) + jtvec_full = serial_sim.Jtvec(model, v, f=f_full) + jtvec_meta = parallel_sim.Jtvec(model, v, f=f_meta) + np.testing.assert_allclose(jtvec_full, jtvec_meta, rtol=1e-6) + + # test get diag + diag_full = serial_sim.getJtJdiag(model, f=f_full) + diag_meta = parallel_sim.getJtJdiag(model, f=f_meta) + np.testing.assert_allclose(diag_full, diag_meta, rtol=1e-6) + + +def test_dask_meta_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + + # split by chunks of sources + chunk_size = 3 + sims = [] + mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + sims.append( + dc.Simulation3DNodal( + mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap(mesh) + ) + ) + mappings.append(maps.IdentityMap(mesh)) + + # incompatible length of mappings and simulations lists + with pytest.raises(ValueError): + DaskMetaSimulation(sims[:-1], mappings, client) + + # Bad Simulation type? + with pytest.raises(TypeError): + DaskRepeatedSimulation( + len(sims) + * [ + lambda x: x * 2, + ], + mappings, + client, + ) + + # mappings have incompatible input lengths: + mappings[0] = maps.Projection(mesh.n_cells + 10, np.arange(mesh.n_cells) + 1) + with pytest.raises(ValueError): + DaskMetaSimulation(sims, mappings, client) + + # incompatible mapping and simulation + mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) + with pytest.raises(ValueError): + DaskMetaSimulation(sims, mappings, client) + + +def test_sum_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) + + mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + + rx1 = gravity.Point(rx_locs, components=["gz"]) + survey1 = gravity.Survey(gravity.SourceField(rx1)) + rx2 = gravity.Point(rx_locs[1:], components=["gz"]) + survey2 = gravity.Survey(gravity.SourceField(rx2)) + + sims = [ + gravity.Simulation3DIntegral( + mesh_bot, + survey=survey1, + rhoMap=maps.IdentityMap(mesh_bot), + n_processes=1, + ), + gravity.Simulation3DIntegral( + mesh_top, + survey=survey2, + rhoMap=maps.IdentityMap(mesh_top), + n_processes=1, + ), + ] + + # Test simulations with different numbers of data. + with pytest.raises(ValueError): + DaskSumMetaSimulation(sims, mappings, client) + + +def test_repeat_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + survey = dc.Survey(src_list) + sim = dc.Simulation3DNodal(mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh)) + + # split by chunks of sources + mappings = [] + for _i in range(10): + mappings.append(maps.IdentityMap(mesh)) + + # mappings have incompatible input lengths: + mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) + with pytest.raises(ValueError): + DaskRepeatedSimulation(sim, mappings, client) + + # incompatible mappings and simulations + mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) + with pytest.raises(ValueError): + DaskRepeatedSimulation(sim, mappings, client) + + # Bad Simulation type? + with pytest.raises(TypeError): + DaskRepeatedSimulation(lambda x: x * 2, mappings, client) diff --git a/tests/meta/test_meta_sim.py b/tests/meta/test_meta_sim.py index 2530efcf7d..2498aeaa36 100644 --- a/tests/meta/test_meta_sim.py +++ b/tests/meta/test_meta_sim.py @@ -150,7 +150,6 @@ def test_sum_sim_correctness(): np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) # test Jtvec - rng = np.random.default_rng(seed=0) v = rng.random(survey.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_mult) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index bc816b0e24..fc1058862d 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -61,6 +61,8 @@ def test_meta_correctness(): serial_sim = MetaSimulation(dc_sims, dc_mappings) parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=12) + rng = np.random.default_rng(seed=0) + try: # create fields objects f_serial = serial_sim.fields(m_test) @@ -72,13 +74,13 @@ def test_meta_correctness(): np.testing.assert_allclose(d_full, d_mult) # test Jvec - u = np.random.rand(mesh.n_cells) + u = rng.random(mesh.n_cells) jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(serial_sim.survey.nD) + v = rng.random(serial_sim.survey.nD) jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) @@ -141,6 +143,8 @@ def test_sum_correctness(): serial_sim = SumMetaSimulation(g_sims, g_mappings) parallel_sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings, n_processes=2) + + rng = np.random.default_rng(0) try: # test fields objects f_serial = serial_sim.fields(m_test) @@ -150,42 +154,42 @@ def test_sum_correctness(): # test data output d_full = serial_sim.dpred(m_test, f=f_serial) d_mult = parallel_sim.dpred(m_test, f=f_parallel) - np.testing.assert_allclose(d_full, d_mult) + np.testing.assert_allclose(d_full, d_mult, rtol=1e-06) # test Jvec - u = np.random.rand(mesh.n_cells) + u = rng.random(mesh.n_cells) jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) - np.testing.assert_allclose(jvec_full, jvec_mult) + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-06) # test Jtvec - v = np.random.rand(survey.nD) + v = rng.random(survey.nD) jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) - np.testing.assert_allclose(jtvec_full, jtvec_mult) + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-06) # test get diag diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) - np.testing.assert_allclose(diag_full, diag_mult) + np.testing.assert_allclose(diag_full, diag_mult, rtol=1e-06) # test things also works without passing optional kwargs parallel_sim.model = m_test d_mult2 = parallel_sim.dpred() - np.testing.assert_allclose(d_mult, d_mult2) + np.testing.assert_allclose(d_mult, d_mult2, rtol=1e-06) jvec_mult2 = parallel_sim.Jvec(m_test, u) - np.testing.assert_allclose(jvec_mult, jvec_mult2) + np.testing.assert_allclose(jvec_mult, jvec_mult2, rtol=1e-06) jtvec_mult2 = parallel_sim.Jtvec(m_test, v) - np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2, rtol=1e-06) parallel_sim._jtjdiag = None diag_mult2 = parallel_sim.getJtJdiag(m_test) - np.testing.assert_allclose(diag_mult, diag_mult2) + np.testing.assert_allclose(diag_mult, diag_mult2, rtol=1e-06) except Exception as err: raise err @@ -216,7 +220,10 @@ def test_repeat_correctness(): parallel_sim = MultiprocessingRepeatedSimulation( grav_sim, repeat_mappings, n_processes=2 ) - t_model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) + + rng = np.random.default_rng(0) + + t_model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) try: # test field things @@ -226,24 +233,24 @@ def test_repeat_correctness(): d_full = serial_sim.dpred(t_model, f_serial) d_repeat = parallel_sim.dpred(t_model, f_parallel) - np.testing.assert_equal(d_full, d_repeat) + np.testing.assert_allclose(d_full, d_repeat, rtol=1e-6) # test Jvec - u = np.random.rand(len(t_model)) + u = rng.random(len(t_model)) jvec_full = serial_sim.Jvec(t_model, u, f=f_serial) jvec_mult = parallel_sim.Jvec(t_model, u, f=f_parallel) - np.testing.assert_allclose(jvec_full, jvec_mult) + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) # test Jtvec - v = np.random.rand(len(sim_ts) * survey.nD) + v = rng.random(len(sim_ts) * survey.nD) jtvec_full = serial_sim.Jtvec(t_model, v, f=f_serial) jtvec_mult = parallel_sim.Jtvec(t_model, v, f=f_parallel) - np.testing.assert_allclose(jtvec_full, jtvec_mult) + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-6) # test get diag diag_full = serial_sim.getJtJdiag(t_model, f=f_serial) diag_mult = parallel_sim.getJtJdiag(t_model, f=f_parallel) - np.testing.assert_allclose(diag_full, diag_mult) + np.testing.assert_allclose(diag_full, diag_mult, rtol=1e-6) except Exception as err: raise err finally: From a0f0e9888207d4a54ab969deec6c1f4014a7929b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 4 Apr 2024 10:21:01 -0700 Subject: [PATCH 392/455] Add Ying and Williams to AUTHORS.rst (#1405) Add @ghwilliams @YingHuuu to `AUTHORS.rst`. --- AUTHORS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 65518b241d..e12ab5a8d8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -24,3 +24,5 @@ - Nick Williams, (`@nwilliams-kobold `_) - John Weis, (`@johnweis0480 `_) - Kalen Martens, (`@kalen-sj `_) +- Williams A. Lima (`@ghwilliams `_) +- Ying Hu, (`@YingHuuu `_) From 64cc32ce3a118f41fcc909b0d449827312269e09 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 4 Apr 2024 12:20:03 -0600 Subject: [PATCH 393/455] Remove link to "twitter" (#1406) #### Summary Remove link to twitter from documentation. --- docs/conf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1e1b4419d6..b7edd86caf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -268,11 +268,6 @@ def linkcode_resolve(domain, info): "url": "https://www.youtube.com/c/geoscixyz", "icon": "fab fa-youtube", }, - { - "name": "Twitter", - "url": "https://twitter.com/simpegpy", - "icon": "fab fa-twitter", - }, ], "use_edit_page_button": False, "collapse_navigation": True, From 00e2b0a10859ff24cf84de359b8990aecbf5c6b3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 4 Apr 2024 11:21:58 -0700 Subject: [PATCH 394/455] Bump Black version to 24.3.0 (#1403) Update black version to 24.3.0 in `environment_test.yml`, requirements files and the configuration file for `pre-commit`. This update was triggered after a moderate vulnerability to Regular Expression Denial of Service (ReDoS). The vulnerability was patched in Black 24.3.0. Run new version of black on code base and docs. Replace lambda definition for function in test due to flake8 error after formatted by the new version of Black. --- .pre-commit-config.yaml | 2 +- SimPEG/directives/__init__.py | 1 + SimPEG/electromagnetics/__init__.py | 1 + .../frequency_domain/__init__.py | 1 + .../frequency_domain/sources.py | 1 - .../natural_source/utils/__init__.py | 1 + .../natural_source/utils/analytic_1d.py | 6 +-- .../static/induced_polarization/__init__.py | 1 + .../static/resistivity/__init__.py | 1 + .../spectral_induced_polarization/__init__.py | 1 + .../electromagnetics/static/utils/__init__.py | 1 + .../electromagnetics/time_domain/__init__.py | 1 + .../time_domain/simulation_1d.py | 6 +-- SimPEG/electromagnetics/utils/__init__.py | 1 + .../__init__.py | 1 + .../simulation.py | 4 +- .../viscous_remanent_magnetization/sources.py | 18 ++----- SimPEG/flow/richards/__init__.py | 1 + SimPEG/flow/richards/empirical.py | 4 +- SimPEG/maps.py | 51 ++++++++++--------- SimPEG/potential_fields/gravity/__init__.py | 1 + .../gravity/_numba_functions.py | 1 + SimPEG/potential_fields/magnetics/__init__.py | 1 + SimPEG/regularization/__init__.py | 1 + .../straight_ray_tomography/__init__.py | 1 + SimPEG/simulation.py | 1 + SimPEG/utils/__init__.py | 1 + SimPEG/utils/pgi_utils.py | 2 +- environment_test.yml | 2 +- examples/01-maps/plot_block_in_layer.py | 1 + examples/01-maps/plot_combo.py | 1 + examples/01-maps/plot_layer.py | 1 + examples/01-maps/plot_mesh2mesh.py | 1 + examples/01-maps/plot_sumMap.py | 1 + examples/02-gravity/plot_inv_grav_tiled.py | 1 + examples/03-magnetics/plot_0_analytic.py | 1 + examples/04-dcip/plot_dc_analytic.py | 1 + examples/06-tdem/plot_fwd_tdem_3d_model.py | 1 + examples/06-tdem/plot_inv_tdem_1D.py | 1 + .../06-tdem/plot_inv_tdem_1D_raw_waveform.py | 1 + examples/09-flow/plot_fwd_flow_richards_1D.py | 1 + examples/09-flow/plot_inv_flow_richards_1D.py | 1 + .../20-published/plot_heagyetal2017_casing.py | 5 +- .../plot_heagyetal2017_cyl_inversions.py | 1 + .../plot_laguna_del_maule_inversion.py | 1 + .../20-published/plot_richards_celia1990.py | 1 + .../plot_schenkel_morrison_casing.py | 1 + .../20-published/plot_vadose_vangenuchten.py | 1 + examples/_archived/plot_inv_grav_linear.py | 1 + examples/_archived/plot_inv_mag_linear.py | 1 + requirements_dev.txt | 2 +- requirements_style.txt | 2 +- tests/base/test_optimizers.py | 10 ++-- tests/em/vrm/test_vrmfwd.py | 1 - tests/em/vrm/test_vrminv.py | 4 +- tests/pf/test_mag_uniform_background_field.py | 1 + .../plot_inv_2_inversion_irls.py | 1 - tutorials/07-fdem/plot_inv_1_em1dfm.py | 1 - tutorials/08-tdem/plot_inv_1_em1dtm.py | 1 - ...t_inv_1_joint_pf_pgi_full_info_tutorial.py | 1 + ...lot_inv_2_joint_pf_pgi_no_info_tutorial.py | 1 + .../plot_inv_1_em1dtm_stitched_skytem.py | 2 +- 62 files changed, 97 insertions(+), 70 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9697f155ad..ccb608b94b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.3.0 hooks: - id: black language_version: python3 diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py index 34c8c1fee7..8ce839d89f 100644 --- a/SimPEG/directives/__init__.py +++ b/SimPEG/directives/__init__.py @@ -96,6 +96,7 @@ DirectiveList """ + from .directives import ( InversionDirective, DirectiveList, diff --git a/SimPEG/electromagnetics/__init__.py b/SimPEG/electromagnetics/__init__.py index c83bedd42b..5deacd0f50 100644 --- a/SimPEG/electromagnetics/__init__.py +++ b/SimPEG/electromagnetics/__init__.py @@ -32,6 +32,7 @@ analytics.getCasingBzMagDipole """ + from scipy.constants import mu_0, epsilon_0 from . import time_domain diff --git a/SimPEG/electromagnetics/frequency_domain/__init__.py b/SimPEG/electromagnetics/frequency_domain/__init__.py index 3dad3cde28..33ed705f2a 100644 --- a/SimPEG/electromagnetics/frequency_domain/__init__.py +++ b/SimPEG/electromagnetics/frequency_domain/__init__.py @@ -73,6 +73,7 @@ fields.FieldsFDEM """ + from .survey import Survey from . import sources from . import receivers diff --git a/SimPEG/electromagnetics/frequency_domain/sources.py b/SimPEG/electromagnetics/frequency_domain/sources.py index 1c739df1b7..95f3c43305 100644 --- a/SimPEG/electromagnetics/frequency_domain/sources.py +++ b/SimPEG/electromagnetics/frequency_domain/sources.py @@ -643,7 +643,6 @@ def s_eDeriv(self, simulation, v, adjoint=False): class MagDipole_Bfield(MagDipole): - """ Point magnetic dipole source calculated with the analytic solution for the fields from a magnetic dipole. No discrete curl is taken, so the magnetic diff --git a/SimPEG/electromagnetics/natural_source/utils/__init__.py b/SimPEG/electromagnetics/natural_source/utils/__init__.py index baae8201b5..79425e954f 100644 --- a/SimPEG/electromagnetics/natural_source/utils/__init__.py +++ b/SimPEG/electromagnetics/natural_source/utils/__init__.py @@ -5,6 +5,7 @@ NOTE: These utilities are not well test, use with care """ + from .solutions_1d import get1DEfields # Add the names of the functions from .analytic_1d import getEHfields, getImpedance from .data_utils import ( diff --git a/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py b/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py index 78de082377..ad50de476f 100644 --- a/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py +++ b/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py @@ -30,9 +30,9 @@ def getEHfields(m1d, sigma, freq, zd, scaleUD=True, scaleValue=1): # Initiate the propagation matrix, in the order down up. UDp = np.zeros((2, m1d.nC + 1), dtype=complex) - UDp[ - 1, 0 - ] = scaleValue # Set the wave amplitude as 1 into the half-space at the bottom of the mesh + UDp[1, 0] = ( + scaleValue # Set the wave amplitude as 1 into the half-space at the bottom of the mesh + ) # Loop over all the layers, starting at the bottom layer for lnr, h in enumerate(m1d.h[0]): # lnr-number of layer, h-thickness of the layer # Calculate diff --git a/SimPEG/electromagnetics/static/induced_polarization/__init__.py b/SimPEG/electromagnetics/static/induced_polarization/__init__.py index 6383f4e05c..b421d9afdb 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/__init__.py +++ b/SimPEG/electromagnetics/static/induced_polarization/__init__.py @@ -20,6 +20,7 @@ The ``induced_polarization`` module makes use of receivers, sources, and surveys defined in the ``SimPEG.electromagnetics.static.resistivity`` module. """ + from .simulation import ( Simulation3DCellCentered, Simulation3DNodal, diff --git a/SimPEG/electromagnetics/static/resistivity/__init__.py b/SimPEG/electromagnetics/static/resistivity/__init__.py index 4e0409892b..9171c0cab9 100644 --- a/SimPEG/electromagnetics/static/resistivity/__init__.py +++ b/SimPEG/electromagnetics/static/resistivity/__init__.py @@ -71,6 +71,7 @@ sources.BaseSrc receivers.BaseRx """ + from .simulation import Simulation3DCellCentered, Simulation3DNodal from .simulation_2d import Simulation2DCellCentered, Simulation2DNodal from .simulation_1d import Simulation1DLayers diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py b/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py index bf5c81630b..9bdeea56be 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py +++ b/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py @@ -60,6 +60,7 @@ simulation_2d.BaseSIPSimulation2D """ + from ....data import Data from .simulation import Simulation3DCellCentered, Simulation3DNodal from .simulation_2d import Simulation2DCellCentered, Simulation2DNodal diff --git a/SimPEG/electromagnetics/static/utils/__init__.py b/SimPEG/electromagnetics/static/utils/__init__.py index a0d69fe512..5245be1490 100644 --- a/SimPEG/electromagnetics/static/utils/__init__.py +++ b/SimPEG/electromagnetics/static/utils/__init__.py @@ -46,6 +46,7 @@ closestPointsGrid """ + from .static_utils import ( electrode_separations, pseudo_locations, diff --git a/SimPEG/electromagnetics/time_domain/__init__.py b/SimPEG/electromagnetics/time_domain/__init__.py index dcf8dde9a8..9c3b41ba21 100644 --- a/SimPEG/electromagnetics/time_domain/__init__.py +++ b/SimPEG/electromagnetics/time_domain/__init__.py @@ -89,6 +89,7 @@ fields.FieldsDerivativesHJ """ + from .simulation import ( Simulation3DMagneticFluxDensity, Simulation3DElectricField, diff --git a/SimPEG/electromagnetics/time_domain/simulation_1d.py b/SimPEG/electromagnetics/time_domain/simulation_1d.py index 83568857e3..afac06ae5b 100644 --- a/SimPEG/electromagnetics/time_domain/simulation_1d.py +++ b/SimPEG/electromagnetics/time_domain/simulation_1d.py @@ -153,9 +153,9 @@ def _compute_coefficients(self): def func(t, i): out = np.zeros_like(t) t = t.copy() - t[ - (t > 0.0) & (t <= t_spline_points.min()) - ] = t_spline_points.min() # constant at very low ts + t[(t > 0.0) & (t <= t_spline_points.min())] = ( + t_spline_points.min() + ) # constant at very low ts out[t > 0.0] = splines[i](np.log(t[t > 0.0])) / t[t > 0.0] return out diff --git a/SimPEG/electromagnetics/utils/__init__.py b/SimPEG/electromagnetics/utils/__init__.py index 25b3f24ac3..d8d4a6182c 100644 --- a/SimPEG/electromagnetics/utils/__init__.py +++ b/SimPEG/electromagnetics/utils/__init__.py @@ -30,6 +30,7 @@ convolve_with_waveform """ + from .waveform_utils import ( omega, k, diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py b/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py index 56349b8aba..2f3f05c1f8 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py +++ b/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py @@ -66,6 +66,7 @@ waveforms.BaseVRMWaveform """ + from . import receivers from . import sources from . import receivers as Rx diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py b/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py index efb063fd5a..35ead8fd9b 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -767,9 +767,7 @@ def _getSubsetAcolumns(self, xyzc, xyzh, pp, qq, refFlag): xyzc[refFlag == qq, :] - xyzh[refFlag == qq, :] / 2 ) # Get bottom southwest corners of cells to be refined m = np.shape(xyzc_sub)[0] - xyzc_sub = np.kron( - xyzc_sub, np.ones((n**3, 1)) - ) # Kron for n**3 refined cells + xyzc_sub = np.kron(xyzc_sub, np.ones((n**3, 1))) # Kron for n**3 refined cells xyzh_sub = np.kron( xyzh_sub / n, np.ones((n**3, 1)) ) # Kron for n**3 refined cells with widths h/n diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py b/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py index 34a660862e..db332af0a7 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py +++ b/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py @@ -138,15 +138,9 @@ def getH0(self, xyz): + m[2] * (xyz[:, 2] - r0[2]) ) - hx0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 0] - r0[0]) * mdotr / r**5 - m[0] / r**3 - ) - hy0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 1] - r0[1]) * mdotr / r**5 - m[1] / r**3 - ) - hz0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 2] - r0[2]) * mdotr / r**5 - m[2] / r**3 - ) + hx0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 0] - r0[0]) * mdotr / r**5 - m[0] / r**3) + hy0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 1] - r0[1]) * mdotr / r**5 - m[1] / r**3) + hz0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 2] - r0[2]) * mdotr / r**5 - m[2] / r**3) return np.c_[hx0, hy0, hz0] @@ -285,8 +279,7 @@ def getH0(self, xyz): (x1p / s) * (x3p * I / (2 * np.pi * s * np.sqrt(x3p**2 + (a + s) ** 2))) * ( - ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) - * spec.ellipe(k) + ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) * spec.ellipe(k) - spec.ellipk(k) ) ) @@ -294,8 +287,7 @@ def getH0(self, xyz): (x2p / s) * (x3p * I / (2 * np.pi * s * np.sqrt(x3p**2 + (a + s) ** 2))) * ( - ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) - * spec.ellipe(k) + ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) * spec.ellipe(k) - spec.ellipk(k) ) ) diff --git a/SimPEG/flow/richards/__init__.py b/SimPEG/flow/richards/__init__.py index b22a2ea880..ec638e997f 100644 --- a/SimPEG/flow/richards/__init__.py +++ b/SimPEG/flow/richards/__init__.py @@ -40,6 +40,7 @@ empirical.VanGenuchtenParams """ + from . import empirical from .survey import Survey from .simulation import SimulationNDCellCentered diff --git a/SimPEG/flow/richards/empirical.py b/SimPEG/flow/richards/empirical.py index edbf7361dd..83b38f33f0 100644 --- a/SimPEG/flow/richards/empirical.py +++ b/SimPEG/flow/richards/empirical.py @@ -570,9 +570,7 @@ def _derivKs(self, u): dKs_dm_p = P_p * self.KsDeriv dKs_dm_n = ( P_n - * utils.sdiag( - theta_e**I * ((1.0 - (1.0 - theta_e ** (1.0 / m)) ** m) ** 2) - ) + * utils.sdiag(theta_e**I * ((1.0 - (1.0 - theta_e ** (1.0 / m)) ** m) ** 2)) * self.KsDeriv ) return dKs_dm_p + dKs_dm_n diff --git a/SimPEG/maps.py b/SimPEG/maps.py index aa0087100a..f4530aefff 100644 --- a/SimPEG/maps.py +++ b/SimPEG/maps.py @@ -1697,9 +1697,7 @@ def getQ(self, alpha): if alpha < 1.0: # oblate spheroid chi = np.sqrt((1.0 / alpha**2.0) - 1) return ( - 1.0 - / 2.0 - * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) + 1.0 / 2.0 * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) ) elif alpha > 1.0: # prolate spheroid chi = np.sqrt(1 - (1.0 / alpha**2.0)) @@ -3273,9 +3271,11 @@ def indActive(self, value): def P(self): if getattr(self, "_P", None) is None: self._P = self.mesh2.get_interpolation_matrix( - self.mesh.cell_centers[self.indActive, :] - if self.indActive is not None - else self.mesh.cell_centers, + ( + self.mesh.cell_centers[self.indActive, :] + if self.indActive is not None + else self.mesh.cell_centers + ), "CC", zeros_outside=True, ) @@ -4581,15 +4581,19 @@ def x(self): if getattr(self, "_x", None) is None: if self.mesh.dim == 1: self._x = [ - self.mesh.cell_centers - if self.indActive is None - else self.mesh.cell_centers[self.indActive] + ( + self.mesh.cell_centers + if self.indActive is None + else self.mesh.cell_centers[self.indActive] + ) ][0] else: self._x = [ - self.mesh.cell_centers[:, 0] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 0] + ( + self.mesh.cell_centers[:, 0] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 0] + ) ][0] return self._x @@ -4605,9 +4609,11 @@ def y(self): if getattr(self, "_y", None) is None: if self.mesh.dim > 1: self._y = [ - self.mesh.cell_centers[:, 1] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 1] + ( + self.mesh.cell_centers[:, 1] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 1] + ) ][0] else: self._y = None @@ -4625,9 +4631,11 @@ def z(self): if getattr(self, "_z", None) is None: if self.mesh.dim > 2: self._z = [ - self.mesh.cell_centers[:, 2] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 2] + ( + self.mesh.cell_centers[:, 2] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 2] + ) ][0] else: self._z = None @@ -5094,12 +5102,7 @@ def _ekblom(self, val): return (val**2 + self.epsilon**2) ** (self.p / 2.0) def _ekblomDeriv(self, val): - return ( - (self.p / 2) - * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) - * 2 - * val - ) + return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val # def _rotation(self, mDict): # if self.mesh.dim == 2: diff --git a/SimPEG/potential_fields/gravity/__init__.py b/SimPEG/potential_fields/gravity/__init__.py index 4a9763bc9d..ae4e97687e 100644 --- a/SimPEG/potential_fields/gravity/__init__.py +++ b/SimPEG/potential_fields/gravity/__init__.py @@ -35,6 +35,7 @@ analytics.GravityGradientSphereFreeSpace """ + from . import survey from . import sources from . import receivers diff --git a/SimPEG/potential_fields/gravity/_numba_functions.py b/SimPEG/potential_fields/gravity/_numba_functions.py index c84069f150..1d6b363b27 100644 --- a/SimPEG/potential_fields/gravity/_numba_functions.py +++ b/SimPEG/potential_fields/gravity/_numba_functions.py @@ -1,6 +1,7 @@ """ Numba functions for gravity simulation using Choclo. """ + import numpy as np try: diff --git a/SimPEG/potential_fields/magnetics/__init__.py b/SimPEG/potential_fields/magnetics/__init__.py index 0db16e066f..6d9241e310 100644 --- a/SimPEG/potential_fields/magnetics/__init__.py +++ b/SimPEG/potential_fields/magnetics/__init__.py @@ -35,6 +35,7 @@ analytics.MagSphereAnaFunA analytics.MagSphereFreeSpace """ + from . import survey from . import sources from . import receivers diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py index 3fa55b9fd3..9026acb078 100644 --- a/SimPEG/regularization/__init__.py +++ b/SimPEG/regularization/__init__.py @@ -147,6 +147,7 @@ BaseAmplitude """ + from ..utils.code_utils import deprecate_class from .base import ( BaseRegularization, diff --git a/SimPEG/seismic/straight_ray_tomography/__init__.py b/SimPEG/seismic/straight_ray_tomography/__init__.py index 68edbf39c3..69e8a06036 100644 --- a/SimPEG/seismic/straight_ray_tomography/__init__.py +++ b/SimPEG/seismic/straight_ray_tomography/__init__.py @@ -24,6 +24,7 @@ """ + from .simulation import Simulation2DIntegral as Simulation from .survey import StraightRaySurvey as Survey from ...survey import BaseSrc as Src diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py index c857aa841e..f6cd144b1a 100644 --- a/SimPEG/simulation.py +++ b/SimPEG/simulation.py @@ -1,6 +1,7 @@ """ Define simulation classes. """ + import os import inspect import numpy as np diff --git a/SimPEG/utils/__init__.py b/SimPEG/utils/__init__.py index c53f37ec75..b023970eca 100644 --- a/SimPEG/utils/__init__.py +++ b/SimPEG/utils/__init__.py @@ -142,6 +142,7 @@ validate_active_indices """ + from discretize.utils.interpolation_utils import interpolation_matrix from .code_utils import ( diff --git a/SimPEG/utils/pgi_utils.py b/SimPEG/utils/pgi_utils.py index 3304b36d4a..eb0658e958 100644 --- a/SimPEG/utils/pgi_utils.py +++ b/SimPEG/utils/pgi_utils.py @@ -1560,7 +1560,7 @@ def __init__( warm_start=warm_start, weights_init=weights_init, update_covariances=update_covariances, - fixed_membership=fixed_membership + fixed_membership=fixed_membership, # **kwargs ) diff --git a/environment_test.yml b/environment_test.yml index 8c5604edb7..84940f92d6 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -40,7 +40,7 @@ dependencies: - choclo # Linters and code style - pre-commit - - black==23.12.1 + - black==24.3.0 - flake8==7.0.0 - flake8-bugbear==23.12.2 - flake8-builtins==2.2.0 diff --git a/examples/01-maps/plot_block_in_layer.py b/examples/01-maps/plot_block_in_layer.py index 4b116ceeb2..c84d27b218 100644 --- a/examples/01-maps/plot_block_in_layer.py +++ b/examples/01-maps/plot_block_in_layer.py @@ -21,6 +21,7 @@ ] """ + import discretize from SimPEG import maps import numpy as np diff --git a/examples/01-maps/plot_combo.py b/examples/01-maps/plot_combo.py index 86a98cf4aa..a7157a4b82 100644 --- a/examples/01-maps/plot_combo.py +++ b/examples/01-maps/plot_combo.py @@ -26,6 +26,7 @@ right). Just to be sure that the derivative is correct, you should always run the test on the mapping that you create. """ + import discretize from SimPEG import maps import numpy as np diff --git a/examples/01-maps/plot_layer.py b/examples/01-maps/plot_layer.py index 90600bde0a..d73a9bc8bf 100644 --- a/examples/01-maps/plot_layer.py +++ b/examples/01-maps/plot_layer.py @@ -17,6 +17,7 @@ 'layer thickness' ] """ + import discretize from SimPEG import maps import numpy as np diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index bb36c19a78..b2063e71bb 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -4,6 +4,7 @@ This mapping allows you to go from one mesh to another. """ + import discretize from SimPEG import maps, utils import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index a07bb8712b..aceb6c9220 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -12,6 +12,7 @@ """ + from discretize import TensorMesh from discretize.utils import active_from_xyz from SimPEG import ( diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index 5ed4cd90e2..37ae5e203d 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -5,6 +5,7 @@ Invert data in tiles. """ + import os import numpy as np import matplotlib.pyplot as plt diff --git a/examples/03-magnetics/plot_0_analytic.py b/examples/03-magnetics/plot_0_analytic.py index 8384445f2e..1c8e7980aa 100644 --- a/examples/03-magnetics/plot_0_analytic.py +++ b/examples/03-magnetics/plot_0_analytic.py @@ -5,6 +5,7 @@ Comparing the magnetics field in Vancouver to Seoul """ + import numpy as np from SimPEG.potential_fields.magnetics import analytics import matplotlib.pyplot as plt diff --git a/examples/04-dcip/plot_dc_analytic.py b/examples/04-dcip/plot_dc_analytic.py index ec16ee2672..da9fca2cb3 100644 --- a/examples/04-dcip/plot_dc_analytic.py +++ b/examples/04-dcip/plot_dc_analytic.py @@ -5,6 +5,7 @@ Comparison of the analytic and numerical solution for a direct current resistivity dipole in 3D. """ + import discretize from SimPEG import utils import numpy as np diff --git a/examples/06-tdem/plot_fwd_tdem_3d_model.py b/examples/06-tdem/plot_fwd_tdem_3d_model.py index 36637e5085..6a07f422ff 100644 --- a/examples/06-tdem/plot_fwd_tdem_3d_model.py +++ b/examples/06-tdem/plot_fwd_tdem_3d_model.py @@ -2,6 +2,7 @@ Time-domain CSEM for a resistive cube in a deep marine setting ============================================================== """ + import empymod import discretize diff --git a/examples/06-tdem/plot_inv_tdem_1D.py b/examples/06-tdem/plot_inv_tdem_1D.py index 95c005e96f..b3f6fd1a78 100644 --- a/examples/06-tdem/plot_inv_tdem_1D.py +++ b/examples/06-tdem/plot_inv_tdem_1D.py @@ -4,6 +4,7 @@ Here we will create and run a TDEM 1D inversion. """ + import numpy as np from SimPEG.electromagnetics import time_domain from SimPEG import ( diff --git a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py index 4a0c5625c9..619ada07e4 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -6,6 +6,7 @@ with VTEM waveform of which initial condition is zero, but have some on- and off-time. """ + import numpy as np import discretize from SimPEG import ( diff --git a/examples/09-flow/plot_fwd_flow_richards_1D.py b/examples/09-flow/plot_fwd_flow_richards_1D.py index 811fc84a46..dcc2a7b7ff 100644 --- a/examples/09-flow/plot_fwd_flow_richards_1D.py +++ b/examples/09-flow/plot_fwd_flow_richards_1D.py @@ -38,6 +38,7 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib import matplotlib.pyplot as plt import numpy as np diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index f30a739c3c..d38dbb4014 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -25,6 +25,7 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib import matplotlib.pyplot as plt import numpy as np diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index 6d0543130c..bcce56721a 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -30,6 +30,7 @@ This example was updated for SimPEG 0.14.0 on January 31st, 2020 by Joseph Capriotti """ + import discretize from SimPEG import utils, maps, tests from SimPEG.electromagnetics import frequency_domain as FDEM, mu_0 @@ -265,8 +266,8 @@ def primaryMapping(self): expMapPrimary * injActMapPrimary # log(sigma) --> sigma * paramMapPrimary # log(sigma) below surface --> include air - * injectCasingParams # parametric --> casing + layered earth - * # parametric layered earth --> parametric + * injectCasingParams # parametric --> casing + layered earth # parametric layered earth --> parametric + * # layered earth + casing self.projectionMapPrimary # grab relevant parameters from full # model (eg. ignore block) diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 98e04747a4..2c93524648 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -18,6 +18,7 @@ This example was updated for SimPEG 0.14.0 on January 31st, 2020 by Joseph Capriotti """ + import discretize from SimPEG import ( maps, diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index d124efa9da..47a467c343 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -11,6 +11,7 @@ Craig Miller """ + import os import shutil import tarfile diff --git a/examples/20-published/plot_richards_celia1990.py b/examples/20-published/plot_richards_celia1990.py index 798ec47149..ce2267d8b9 100644 --- a/examples/20-published/plot_richards_celia1990.py +++ b/examples/20-published/plot_richards_celia1990.py @@ -39,6 +39,7 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib.pyplot as plt import numpy as np diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index 4868459e2f..478d8b90e3 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -44,6 +44,7 @@ a citation would be much appreciated! """ + import matplotlib.pylab as plt import numpy as np import discretize diff --git a/examples/20-published/plot_vadose_vangenuchten.py b/examples/20-published/plot_vadose_vangenuchten.py index 95b8d10af3..a05cb0b6f4 100644 --- a/examples/20-published/plot_vadose_vangenuchten.py +++ b/examples/20-published/plot_vadose_vangenuchten.py @@ -10,6 +10,7 @@ The RETC code for quantifying the hydraulic functions of unsaturated soils, Van Genuchten, M Th, Leij, F J, Yates, S R """ + import matplotlib.pyplot as plt import discretize diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index c11bea15e9..c35831bd76 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -6,6 +6,7 @@ with a compact norm """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 661ed94062..bf25676a50 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -6,6 +6,7 @@ with a compact norm """ + import matplotlib.pyplot as plt import numpy as np from discretize import TensorMesh diff --git a/requirements_dev.txt b/requirements_dev.txt index f81dbe7d4e..5bab4a0c4b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -19,7 +19,7 @@ jupyter toolz empymod>=2.0.0 scooby -black==23.12.1 +black==24.3.0 pre-commit twine memory_profiler diff --git a/requirements_style.txt b/requirements_style.txt index 86051e527b..a4fd699571 100644 --- a/requirements_style.txt +++ b/requirements_style.txt @@ -1,4 +1,4 @@ -black==23.12.1 +black==24.3.0 flake8==7.0.0 flake8-bugbear==23.12.2 flake8-builtins==2.2.0 diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index 4bdfd2cafa..212d433787 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -49,11 +49,11 @@ def test_ProjGradient_quadratic1Bound(self): self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) def test_NewtonRoot(self): - fun = ( - lambda x, return_g=True: np.sin(x) - if not return_g - else (np.sin(x), sdiag(np.cos(x))) - ) + def fun(x, return_g=True): + if return_g: + return np.sin(x), sdiag(np.cos(x)) + return np.sin(x) + x = np.array([np.pi - 0.3, np.pi + 0.1, 0]) xopt = optimization.NewtonRoot(comments=False).root(fun, x) x_true = np.array([np.pi, np.pi, 0]) diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 89f1fc54f9..7bcef1c9e0 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -5,7 +5,6 @@ class VRM_fwd_tests(unittest.TestCase): - """ Computed vs analytic dipole field """ diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index b4f316c02f..15c022c926 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -63,9 +63,7 @@ def test_basic_inversion(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=Problem) W = ( - mkvc( - (np.sum(np.array(Problem.A) ** 2, axis=0)) / meshObj.cell_volumes**2.0 - ) + mkvc((np.sum(np.array(Problem.A) ** 2, axis=0)) / meshObj.cell_volumes**2.0) ** 0.25 ) reg = regularization.WeightedLeastSquares( diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index 18989d4d09..d4e72bae40 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -1,6 +1,7 @@ """ Test the UniformBackgroundField class """ + import pytest from SimPEG.potential_fields.magnetics import UniformBackgroundField, SourceField diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index 15c7f18dc5..4241d5d886 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -16,7 +16,6 @@ """ - import numpy as np import matplotlib.pyplot as plt diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index 2d566f06a7..d222fd9d62 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -18,7 +18,6 @@ """ - ######################################################################### # Import modules # -------------- diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index 535f646747..9e189b32cb 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -18,7 +18,6 @@ """ - ######################################################################### # Import modules # -------------- diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index 96b83c4b36..c6e170bf27 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -22,6 +22,7 @@ `_. """ + ######################################################################### # Import modules # -------------- diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index ba0219e59a..0e57205665 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -23,6 +23,7 @@ `_. """ + ######################################################################### # Import modules # -------------- diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 7cdb188cba..6419f37d23 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -455,7 +455,7 @@ def PolygonInd(mesh, pts): ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) norm = mpl.colors.Normalize( vmin=np.log10(true_model.min()), - vmax=np.log10(true_model.max()) + vmax=np.log10(true_model.max()), # vmin=np.log10(0.1), vmax=np.log10(1) ) cbar = mpl.colorbar.ColorbarBase( From 843ed3507ad25d66d0d1ab14e8c70e09d7dabeb1 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 9 Apr 2024 09:40:55 -0700 Subject: [PATCH 395/455] Add release notes for SimPEG v0.21 (#1409) Add release notes for SimPEG v0.21.0 to the docs. --------- Co-authored-by: Joseph Capriotti --- docs/content/release/0.21.0-notes.rst | 274 ++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 2 files changed, 275 insertions(+) create mode 100644 docs/content/release/0.21.0-notes.rst diff --git a/docs/content/release/0.21.0-notes.rst b/docs/content/release/0.21.0-notes.rst new file mode 100644 index 0000000000..c317ded823 --- /dev/null +++ b/docs/content/release/0.21.0-notes.rst @@ -0,0 +1,274 @@ +.. _0.21.0_notes: + +=========================== +SimPEG 0.21.0 Release Notes +=========================== + +April 8th, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Gravity simulation using Choclo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now we can use a faster and more memory efficient implementation of the gravity +simulation ``SimPEG.potential_fields.gravity.Simulation3DIntegral``, making use +of Choclo and Numba. To make use of this functionality you will need to +`install ``choclo`` `__ in +addition to ``SimPEG``. + +See https://github.com/simpeg/simpeg/pull/1285. + +Use Dask with MetaSimulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new ``SimPEG.meta.DaskMetaSimulation`` class has been added that allows to +use Dask with ``SimPEG.meta.MetaSimulations``. + +See https://github.com/simpeg/simpeg/pull/1199. + +Rotated Gradients +~~~~~~~~~~~~~~~~~ + +Added a new ``SimPEG.regularization.SmoothnessFullGradient`` regularization +class that allows to regularize first order smoothness along any arbitrary +direction, enabling anisotropic weighting. This regularization also works for +a ``SimplexMesh``. + +See https://github.com/simpeg/simpeg/pull/1167. + +Logistic Sigmoid Map +~~~~~~~~~~~~~~~~~~~~ + +New ``SimPEG.map.LogisticSigmoidMap`` mapping class that computes the logistic +sigmoid of the model parameters. This is an alternative method to incorporate +upper and lower bounds on model parameters. + +See https://github.com/simpeg/simpeg/pull/1352. + +Create Jacobian matrix in NSEM and FDEM simulations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The frequency domain electromagnetic simulations (including natural source) now +support creating and storing the Jacobian matrix. You can access it by using +the ``getJ`` method. + +See https://github.com/simpeg/simpeg/pull/1276. + + +Documentation +------------- + +This new release includes major improvements in documentation pages: more +detailed docstrings of classes and methods, the addition of directive classes +to the API reference, improvements to the contributing guide, among corrections +and fixes. + + +Breaking changes +---------------- + +Removal of deprecated bits +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several deprecated bits of code has been removed in this release. From old +classes, methods and properties that were marked for deprecation a few releases +back. These removals simplify the SimPEG API and cleans up the codebase. + +Remove factor of half in data misfits and regularizations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Simplify the definition of data misfit and regularization terms by removing the +leading factor of one half from these functions. This change makes it easier to +interpret the resulting values of these objective functions, while +avoiding confusions with their definition. + +See https://github.com/simpeg/simpeg/pull/1326. + + +Bugfixes +-------- + +A few bugs have been fixed: + +- Fix issue with lengthscales in coterminal angle calculations by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1299 +- ISSUE-1341: Set parent of objective functions by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1342 +- Ravel instead of flatten by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1343 +- Fix implementation of coterminal function by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1334 +- Simpeg vector update by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1329 + + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +* `@ckohnke `__ +* `@dccowan `__ +* `@domfournier `__ +* `@ghwilliams `__ +* `@jcapriot `__ +* `@JKutt `__ +* `@johnweis0480 `__ +* `@lheagy `__ +* `@mplough-kobold `__ +* `@santisoler `__ +* `@thibaut-kobold `__ +* `@YingHuuu `__ + +We would like to highlight the contributions made by new contributors: + +- `@mplough-kobold `__ made their first + contribution in https://github.com/simpeg/simpeg/pull/1282 +- `@ghwilliams `__ made their first contribution + in https://github.com/simpeg/simpeg/pull/1292 +- `@johnweis0480 `__ made their first + contribution in https://github.com/simpeg/simpeg/pull/1329 +- `@ckohnke `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1352 +- `@YingHuuu `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1344 + + +Pull Requests +============= + +- Add 0.20.0 release notes to toc by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1277 +- add plausible analytics to simpeg docs by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1279 +- Refresh links in documentation by `@mplough-kobold `__ in + https://github.com/simpeg/simpeg/pull/1282 +- Run pytest on Azure with increased verbosity by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1287 - Allow to use random seed in make_synthetic_data by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1286 +- pgi doc by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1291 +- Fix deprecation warning for gradientType in SparseSmoothness by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1284 +- Gravity simulation with Choclo as engine by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1285 +- Fix minor flake8 warning by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1307 +- ISSUE-1298: Use normal distributed noise in example. by `@domfournier `__ + in https://github.com/simpeg/simpeg/pull/1312 +- Ditch deprecated functions in utils.model_builder by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1311 - Triaxial magnetic gradient forward modelling by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1288 +- Documentation improvements for classes in Objective Function Pieces + by `@ghwilliams `__ in https://github.com/simpeg/simpeg/pull/1292 +- Fix description of source_field in gravity survey by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1322 +- Add ``weights_keys`` method to ``BaseRegularization`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1320 +- Bump versions of flake8 and black and pin flake plugins by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1330 +- Move ``__init__`` in ``BaseSimulation`` to the top of the class by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1323 +- Simpeg vector update by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1329 +- Fix typo in error messages by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1324 +- Fix issue with lengthscales in coterminal angle calculations by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1299 +- Simplify check for invalid multipliers by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1336 +- Ravel instead of flatten by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1343 +- Fix implementation of coterminal function by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1334 +- Update cross gradient hessian approximation by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1355 +- ISSUE-1341: Set parent of objective functions by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1342 +- Fix partial derivatives in regularization docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1362 +- Remove factor of half in data misfits and regularizations by `@lheagy `__ + in https://github.com/simpeg/simpeg/pull/1326 +- Improvements to template for a bug report issue by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1359 +- Simplify a few gravity simulation tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1363 +- Exponential Sinusoids Simulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1337 +- Replace magnetic SourceField for UniformBackgroundField by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1364 +- Remove deprecated regularization classes by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1365 +- Removed deprecated properties of UpdateSensitivityWeights by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1368 +- Replace indActive for active_cells in regularizations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1366 +- Remove the debug argument from InversionDirective by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1370 +- Remove cellDiff properties of RegularizationMesh by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1371 +- Remove deprecated bits of code by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1372 +- Use choclo in gravity tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1378 +- Remove surface2ind_topo by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1374 +- Speed up sphinx documentation building by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1382 +- Add docs/sg_execution_times.rst to .gitignore by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1380 +- Describe merge process of Pull Requests in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1375 +- Simplify private methods in gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1384 +- Update Slack links: point to Mattermost by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1385 +- added getJ for fdem and nsem simulations by `@JKutt `__ in + https://github.com/simpeg/simpeg/pull/1276 +- Add LogisticSigmoidMap by `@ckohnke `__ in + https://github.com/simpeg/simpeg/pull/1352 +- Remove the cell_weights attribute in regularizations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1376 +- Remove regmesh, mref and gradientType from regularizations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1377 +- Test if gravity sensitivities are stored on disk by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1388 +- Check if mesh is 3D when using Choclo in gravity simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1386 +- Rotated Gradients by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1167 +- Add directives to the API Reference by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1397 +- Remove deprecated modelType in mag simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1399 +- Remove mref property of PGI regularization by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1400 +- Add link to User Tutorials to navbar in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1401 +- Improve documentation for base simulation classes by `@ghwilliams `__ in + https://github.com/simpeg/simpeg/pull/1295 +- Enforce regularization ``weights`` as dictionaries by `@YingHuuu `__ in + https://github.com/simpeg/simpeg/pull/1344 +- Minor adjustments to Sphinx configuration by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1398 +- Update AUTHORS.rst by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1259 +- Update year in LICENSE by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1404 +- Dask MetaSim by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1199 +- Add Ying and Williams to AUTHORS.rst by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1405 +- Remove link to “twitter” by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1406 +- Bump Black version to 24.3.0 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1403 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 98e30d6b57..bd5a9b9ed0 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.21.0 <0.21.0-notes> 0.20.0 <0.20.0-notes> 0.19.0 <0.19.0-notes> 0.18.1 <0.18.1-notes> From de79ed07a8075ab6f8b18735e90b82c2e207b7e8 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 9 Apr 2024 12:29:54 -0600 Subject: [PATCH 396/455] Publish documentation on azure (#1412) Adds a publish step on the documentation test CI so we can download it and view it. --- .azure-pipelines/matrix.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index fd6cc259a3..2b96f56561 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -50,6 +50,13 @@ jobs: pytest ${{ test }} -v --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning displayName: 'Testing ${{ test }}' + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/docs/_build/html + artifactName: html_docs + displayName: 'Publish documentation artifact' + condition: eq('${{ test }}', 'tests/docs -s -v') + - script: | curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov From ee05f30eb66a2496f9f0dbc40f3b70985a556384 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 10 Apr 2024 10:57:23 -0600 Subject: [PATCH 397/455] Fix hard dask dependency (#1415) #### Summary Fixes unintended `dask` hard dependency, --- SimPEG/meta/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/SimPEG/meta/__init__.py b/SimPEG/meta/__init__.py index 9dab54e5be..d78117a2e4 100644 --- a/SimPEG/meta/__init__.py +++ b/SimPEG/meta/__init__.py @@ -75,4 +75,20 @@ MultiprocessingRepeatedSimulation, ) -from .dask_sim import DaskMetaSimulation, DaskSumMetaSimulation, DaskRepeatedSimulation +try: + from .dask_sim import ( + DaskMetaSimulation, + DaskSumMetaSimulation, + DaskRepeatedSimulation, + ) +except ImportError: + + class DaskMetaSimulation(MetaSimulation): + def __init__(self, *args, **kwargs): + raise ImportError( + "This simulation requires dask.distributed. Please see installation " + "instructions at https://distributed.dask.org/" + ) + + DaskSumMetaSimulation = DaskMetaSimulation + DaskRepeatedMetaSimulation = DaskMetaSimulation From 5cd200d646aa40f039ce6c8c1a80c5551e95f1aa Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 10 Apr 2024 11:57:15 -0700 Subject: [PATCH 398/455] Add release notes for v0.21.1 (#1416) Add release notes for patch `v0.21.1`. --- docs/content/release/0.21.0-notes.rst | 2 ++ docs/content/release/0.21.1-notes.rst | 30 +++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 3 files changed, 33 insertions(+) create mode 100644 docs/content/release/0.21.1-notes.rst diff --git a/docs/content/release/0.21.0-notes.rst b/docs/content/release/0.21.0-notes.rst index c317ded823..f3ec79376e 100644 --- a/docs/content/release/0.21.0-notes.rst +++ b/docs/content/release/0.21.0-notes.rst @@ -272,3 +272,5 @@ Pull Requests https://github.com/simpeg/simpeg/pull/1406 - Bump Black version to 24.3.0 by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1403 +- Publish documentation on azure `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1412 diff --git a/docs/content/release/0.21.1-notes.rst b/docs/content/release/0.21.1-notes.rst new file mode 100644 index 0000000000..cd35017d87 --- /dev/null +++ b/docs/content/release/0.21.1-notes.rst @@ -0,0 +1,30 @@ +.. _0.21.1_notes: + +=========================== +SimPEG 0.21.1 Release Notes +=========================== + +April 10th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +Minor fix when importing Dask in the ``meta`` module: Dask is an optional +dependency. + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +* `@jcapriot `__ + +Pull Requests +============= + +* Fix hard dask dependency by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1415 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index bd5a9b9ed0..49daf1cfc9 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.21.1 <0.21.1-notes> 0.21.0 <0.21.0-notes> 0.20.0 <0.20.0-notes> 0.19.0 <0.19.0-notes> From fa5d79475f2a9b5656364e10508cdb85c65e35d5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 11 Apr 2024 09:22:03 -0700 Subject: [PATCH 399/455] Remove the parameters argument from docstring (#1417) Remove the old `parameters` argument from the docstrings of the `UniformBackgroundField` class. --- SimPEG/potential_fields/magnetics/sources.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/SimPEG/potential_fields/magnetics/sources.py b/SimPEG/potential_fields/magnetics/sources.py index c62bc80b23..33c9137ea3 100644 --- a/SimPEG/potential_fields/magnetics/sources.py +++ b/SimPEG/potential_fields/magnetics/sources.py @@ -12,9 +12,6 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- receiver_list : list of SimPEG.potential_fields.magnetics.Point - parameters : tuple of (amplitude, inclutation, declination), optional - Deprecated input for the function, provided in this position for backwards - compatibility amplitude : float, optional amplitude of the inducing backgound field, usually this is in units of nT. inclination : float, optional From 999d18c03ebe8b04eae28d7397d8641b2259c5bd Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 16 Apr 2024 16:24:29 -0600 Subject: [PATCH 400/455] Use reviewdog to annotate PR's with black and flake8 errors. (#1424) #### Summary Uses reviewdog to annotate PR's with suggested changes from black, and comment on errors from flake8 #### What does this implement/fix? Allows for automated reviewing of PR's from reviewdog detailing the flake8 and black errors when they fail, making it much more obvious to people who have submitted PR's about why those tests are failing. --- .github/workflows/pull_request.yml | 38 ++++++++++++++++++++++++++++++ azure-pipelines.yml | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000000..026bf825c5 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,38 @@ +name : Reviewdog PR Annotations +on: [pull_request] + +jobs: + flake8: + runs-on: ubuntu-latest + name: Flake8 check + steps: + - name: Checkout source repository + uses: actions/checkout@v4 + - name: Setup Python env + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies to run the flake8 checks + run: pip install -r requirements_style.txt + - name: flake8 review + uses: reviewdog/action-flake8@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review + + black: + name: Black check + runs-on: ubuntu-latest + steps: + - name: Checkout source repository + uses: actions/checkout@v4 + - name: Setup Python env + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies to run the black checks + run: pip install -r requirements_style.txt + - uses: reviewdog/action-black@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2b6d0dcb57..f7f7ed21a9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -141,4 +141,4 @@ stages: git push displayName: Push documentation to simpeg-docs env: - GH_TOKEN: $(gh.token) + GH_TOKEN: $(gh.token) \ No newline at end of file From 8a763d3dd3f620628c3fb0faf9fb1b359e219f12 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 17 Apr 2024 10:40:14 -0600 Subject: [PATCH 401/455] Safely run reviewdog on `pull_request_target` events (#1427) #### Summary Updates reviewdog GitHub annotation action to safely run from pull request target events --- .github/workflows/pull_request.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 026bf825c5..0151372c8d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,22 +1,32 @@ name : Reviewdog PR Annotations -on: [pull_request] +on: [pull_request_target] jobs: flake8: runs-on: ubuntu-latest name: Flake8 check steps: - - name: Checkout source repository + - name: Checkout target repository source uses: actions/checkout@v4 + - name: Setup Python env uses: actions/setup-python@v5 with: python-version: '3.11' + - name: Install dependencies to run the flake8 checks run: pip install -r requirements_style.txt + + - name: checkout pull request source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: pr_source + - name: flake8 review uses: reviewdog/action-flake8@v3 with: + workdir: pr_source github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review @@ -24,15 +34,25 @@ jobs: name: Black check runs-on: ubuntu-latest steps: - - name: Checkout source repository + - name: Checkout target repository source uses: actions/checkout@v4 + - name: Setup Python env uses: actions/setup-python@v5 with: python-version: '3.11' + - name: Install dependencies to run the black checks run: pip install -r requirements_style.txt + + - name: checkout pull request source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: 'pr_source' + - uses: reviewdog/action-black@v3 with: + workdir: 'pr_source' github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review \ No newline at end of file From fbd92e5ef05b2e0ac49c736aaea6c7dd7731eb8f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 25 Apr 2024 10:15:05 -0700 Subject: [PATCH 402/455] Add new Issue template for making a release (#1410) Add a new issue template for making a new release, containing a checklist of the steps needed to make a new release. --------- Co-authored-by: Joseph Capriotti --- .github/ISSUE_TEMPLATE/release-checklist.md | 151 ++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/release-checklist.md diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md new file mode 100644 index 0000000000..d173424945 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -0,0 +1,151 @@ +--- +name: Release checklist +about: "Maintainers only: Checklist for making a new release" +title: "Release vX.Y.Z" +labels: "maintenance" +assignees: "" +--- + + + +**Target date:** YYYY/MM/DD + +## Generate release notes + +### Autogenerate release notes with GitHub + +- [ ] Generate a draft for a new Release in GitHub. +- [ ] Create a new tag for it (the version number with a leading `v`). +- [ ] Generate release notes automatically. +- [ ] Copy those notes and paste them into a `notes.md` file. +- [ ] Discard the draft (we'll generate a new one later on). + +### Add release notes to the docs + +- [ ] Convert the Markdown file to RST with: `pandoc notes.md -o notes.rst`. +- [ ] Generate list of contributors from the release notes with: + ```bash + grep -Eo "@[[:alnum:]-]+" notes.rst | sort -u | sed -E 's/^/* /' + ``` + Paste the list into the file under a new `Contributors` category. +- [ ] Check if every contributor that participated in the release is in the + list. Generate a list of authors and co-authors from the git log with (update + the `last_release`): + ```bash + export last_release="v0.20.0" + git shortlog HEAD...$last_release -sne > contributors + git log HEAD...$last_release | grep "Co-authored-by" | sed 's/Co-authored-by://' | sed 's/^[[:space:]]*/ /' | sort | uniq -c | sort -nr | sed 's/^ //' >> contributors + sort -rn contributors + ``` +- [ ] Transform GitHub handles into links to their profiles: + ```bash + sed -Ei 's/@([[:alnum:]-]+)/`@\1 `__/' notes.rst + ``` +- [ ] Copy the content of `notes.rst` to a new file + `docs/content/release/-notes.rst`. +- [ ] Edit the release notes file, following the template below and the + previous release notes. +- [ ] Add the new release notes to the list in `docs/content/release/index.rst`. +- [ ] Open a PR with the new release notes. +- [ ] Manually view the built documentation by downloading the Azure `html_doc` + artifact and check for formatting and errors. +- [ ] Merge that PR + + +
+Template for release notes: + +```rst +.. __notes: + +=========================== +SimPEG Release Notes +=========================== + +MONTH DAYth, YEAR + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +.. + list new features under subheadings, include link to related PRs + +Documentation +------------- + +.. + list improvements to documentation + +Bugfixes +-------- + +.. + list bugfixes, include link to related PRs + +Breaking changes +---------------- + +.. + list breaking changes introduced in this new release, include link to + releated PRs + +Contributors +============ + +.. + paste list of contributors that was generated in `notes.rst` + +Pull Requests +============= + +.. + paste list of PRs that were copied to `notes.rst` +``` + +
+ + +## Make the new release + +- [ ] Draft a new GitHub Release +- [ ] Create a new tag for it (the version number with a leading `v`). +- [ ] Target the release on `main` or on a particular commit from `main` +- [ ] Generate release notes automatically. +- [ ] Publish the release + +## Extra tasks + +After publishing the release, Azure will automatically push the new version to +PyPI, and build and deploy the docs. You can check the progress of these tasks +in: https://dev.azure.com/simpeg/simpeg/_build + +After they finish: + +- [ ] Check the new version is available in PyPI: https://pypi.org/project/SimPEG/ +- [ ] Check the new documentation is online: https://docs.simpeg.xyz + +For the new version to be available in conda-forge, we need to update the +[conda-forge/simpeg-feedstock](https://github.com/conda-forge/simpeg-feedstock) +repository. Within the same day of the release a new PR will be automatically +open in that repository. So: + +- [ ] Follow the steps provided in the checklist in that PR and merge it. +- [ ] Make sure the new version is available through conda-forge: https://anaconda.org/conda-forge/simpeg + +Lastly, we would need to update the SimPEG version used in +[`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) and rerun +its notebooks: + +- [ ] Open issue in + [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) for + rerunning the notebooks using the new released version of SimPEG + +Finally: + +- [ ] Close this issue From b85f758b216a32a5f4df06122cac22e67999b06b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 25 Apr 2024 12:03:33 -0700 Subject: [PATCH 403/455] Replace use of `refine_tree_xyz` in DCIP tutorials (#1381) Replace the use of `refine_tree_xyz` in DC-IP tutorials for their corresponding counterpart methods of discretize meshes: `refine_bounding_box`, `refine_points` and `refine_surface`. Fixes #1379 --- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 16 +++++----------- tutorials/05-dcr/plot_inv_2_dcr2d.py | 16 +++++----------- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 16 +++++----------- tutorials/06-ip/plot_fwd_2_dcip2d.py | 16 +++++----------- tutorials/06-ip/plot_inv_2_dcip2d.py | 16 +++++----------- 5 files changed, 25 insertions(+), 55 deletions(-) diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 2e7935463b..747e60e024 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -22,7 +22,7 @@ # from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz from SimPEG.utils import model_builder from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc @@ -123,11 +123,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -144,16 +142,12 @@ np.reshape(electrode_locations, (4 * survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) # Refine core mesh region xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh = refine_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index a0479907ad..fd47d61bc2 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -29,7 +29,7 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz from SimPEG.utils import model_builder from SimPEG import ( @@ -173,11 +173,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -194,16 +192,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) # Refine core mesh region xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh = refine_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index d72de43fb9..a6a4024e1f 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -29,7 +29,7 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz from SimPEG.utils import model_builder from SimPEG import ( @@ -179,11 +179,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -200,16 +198,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) # Refine core mesh region xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh = refine_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index 0631392f6d..ada51a11ef 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -30,7 +30,7 @@ # from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz from SimPEG.utils import model_builder from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc @@ -135,11 +135,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -156,16 +154,12 @@ np.reshape(electrode_locations, (4 * dc_survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) # Refine core mesh region xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh = refine_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index ed14b4fcbe..fe274ce764 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -33,7 +33,7 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz from SimPEG.utils import model_builder from SimPEG import ( @@ -188,11 +188,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -209,16 +207,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) # Refine core mesh region xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh = refine_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() From 07aeb65de865cd9df55e2d51ca840ea9d60f2234 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 25 Apr 2024 12:05:26 -0700 Subject: [PATCH 404/455] Fix rst syntax in release notes for v0.21.0 (#1434) Fix rst syntax in the release notes of v0.21.0. Fixes #1433 --- docs/content/release/0.21.0-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/release/0.21.0-notes.rst b/docs/content/release/0.21.0-notes.rst index f3ec79376e..5e668d9dee 100644 --- a/docs/content/release/0.21.0-notes.rst +++ b/docs/content/release/0.21.0-notes.rst @@ -21,7 +21,7 @@ Gravity simulation using Choclo Now we can use a faster and more memory efficient implementation of the gravity simulation ``SimPEG.potential_fields.gravity.Simulation3DIntegral``, making use of Choclo and Numba. To make use of this functionality you will need to -`install ``choclo`` `__ in +`install Choclo `__ in addition to ``SimPEG``. See https://github.com/simpeg/simpeg/pull/1285. From 8ab946ea42a22eed77b10cf3ea454d6a9c6183f6 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 25 Apr 2024 17:36:48 -0600 Subject: [PATCH 405/455] Move to a PEP8 compliant package name. (#1430) Change package name from `SimPEG` to `simpeg` --- .coveragerc | 2 +- .github/ISSUE_TEMPLATE/bug-report.yml | 6 +- .github/ISSUE_TEMPLATE/post-install.yml | 2 +- .gitignore | 2 +- Makefile | 8 +- README.rst | 10 +-- SimPEG.py | 11 +++ SimPEG/dask/__init__.py | 10 --- docs/Makefile | 8 +- docs/README.md | 2 +- docs/conf.py | 16 ++-- docs/content/release/0.14.0-notes.rst | 34 ++++----- docs/content/release/0.14.1-notes.rst | 4 +- docs/content/release/0.14.2-notes.rst | 8 +- docs/content/release/0.14.3-notes.rst | 10 +-- docs/content/release/0.15.0-notes.rst | 6 +- docs/content/release/0.18.0-notes.rst | 2 +- docs/content/release/0.19.0-notes.rst | 8 +- docs/content/release/0.21.0-notes.rst | 10 +-- docs/make.bat | 4 +- examples/01-maps/plot_block_in_layer.py | 2 +- examples/01-maps/plot_combo.py | 6 +- examples/01-maps/plot_layer.py | 2 +- examples/01-maps/plot_mesh2mesh.py | 2 +- examples/01-maps/plot_sumMap.py | 4 +- examples/02-gravity/plot_inv_grav_tiled.py | 6 +- examples/03-magnetics/plot_0_analytic.py | 2 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 10 +-- .../plot_inv_mag_MVI_VectorAmplitude.py | 10 +-- .../plot_inv_mag_nonLinear_Amplitude.py | 14 ++-- examples/04-dcip/plot_dc_analytic.py | 6 +- ...cip_dipoledipole_3Dinversion_twospheres.py | 6 +- ..._dcip_dipoledipole_parametric_inversion.py | 6 +- .../plot_read_DC_data_with_IO_class.py | 6 +- examples/05-fdem/plot_0_fdem_analytic.py | 6 +- .../plot_inv_fdem_loop_loop_2Dinversion.py | 12 +-- examples/06-tdem/plot_0_tdem_analytic.py | 6 +- examples/06-tdem/plot_fwd_tdem_3d_model.py | 16 ++-- ...fwd_tdem_inductive_src_permeable_target.py | 6 +- examples/06-tdem/plot_fwd_tdem_waveforms.py | 4 +- examples/06-tdem/plot_inv_tdem_1D.py | 4 +- .../06-tdem/plot_inv_tdem_1D_raw_waveform.py | 6 +- examples/07-nsem/plot_fwd_nsem_MTTipper3D.py | 8 +- examples/08-vrm/plot_fwd_vrm.py | 6 +- examples/08-vrm/plot_inv_vrm_eq.py | 4 +- examples/09-flow/plot_fwd_flow_richards_1D.py | 4 +- examples/09-flow/plot_inv_flow_richards_1D.py | 18 ++--- examples/10-pgi/plot_inv_0_PGI_Linear_1D.py | 4 +- ...1_PGI_Linear_1D_joint_WithRelationships.py | 2 +- .../plot_booky_1D_time_freq_inv.py | 6 +- .../plot_booky_1Dstitched_resolve_inv.py | 6 +- .../plot_effective_medium_theory.py | 4 +- .../20-published/plot_heagyetal2017_casing.py | 8 +- .../plot_heagyetal2017_cyl_inversions.py | 6 +- .../plot_laguna_del_maule_inversion.py | 8 +- examples/20-published/plot_load_booky.py | 2 +- .../20-published/plot_richards_celia1990.py | 4 +- .../plot_schenkel_morrison_casing.py | 6 +- .../plot_tomo_joint_with_volume.py | 4 +- .../20-published/plot_vadose_vangenuchten.py | 2 +- .../_archived/plot_inv_dcip_2_5Dinversion.py | 10 +-- ...lot_inv_dcip_dipoledipole_2_5Dinversion.py | 8 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 8 +- examples/_archived/plot_inv_grav_linear.py | 8 +- examples/_archived/plot_inv_mag_linear.py | 6 +- setup.py | 5 +- {SimPEG => simpeg}/__init__.py | 6 +- {SimPEG => simpeg}/base/__init__.py | 0 {SimPEG => simpeg}/base/pde_simulation.py | 0 simpeg/dask/__init__.py | 10 +++ .../dask/electromagnetics/__init__.py | 0 .../dask/electromagnetics/static/__init__.py | 0 .../static/induced_polarization/__init__.py | 0 .../static/induced_polarization/simulation.py | 0 .../static/resistivity/__init__.py | 0 .../static/resistivity/simulation.py | 0 .../dask/potential_fields/__init__.py | 0 .../dask/potential_fields/base.py | 0 .../dask/potential_fields/gravity/__init__.py | 0 .../potential_fields/gravity/simulation.py | 0 .../potential_fields/magnetics/__init__.py | 0 .../potential_fields/magnetics/simulation.py | 0 {SimPEG => simpeg}/dask/simulation.py | 0 {SimPEG => simpeg}/dask/utils.py | 0 {SimPEG => simpeg}/data.py | 8 +- {SimPEG => simpeg}/data_misfit.py | 22 +++--- {SimPEG => simpeg}/directives/__init__.py | 8 +- {SimPEG => simpeg}/directives/directives.py | 34 ++++----- .../directives/pgi_directives.py | 8 +- .../directives/sim_directives.py | 0 .../electromagnetics/__init__.py | 4 +- .../electromagnetics/analytics/DC.py | 0 .../electromagnetics/analytics/FDEM.py | 6 +- .../analytics/FDEMDipolarfields.py | 2 +- .../electromagnetics/analytics/FDEMcasing.py | 2 +- .../electromagnetics/analytics/NSEM.py | 2 +- .../electromagnetics/analytics/TDEM.py | 2 +- .../electromagnetics/analytics/__init__.py | 0 {SimPEG => simpeg}/electromagnetics/base.py | 6 +- .../electromagnetics/base_1d.py | 0 .../frequency_domain/__init__.py | 4 +- .../frequency_domain/fields.py | 26 +++---- .../frequency_domain/receivers.py | 8 +- .../frequency_domain/simulation.py | 12 +-- .../frequency_domain/simulation_1d.py | 0 .../frequency_domain/sources.py | 14 ++-- .../frequency_domain/survey.py | 2 +- .../natural_source/__init__.py | 4 +- .../electromagnetics/natural_source/fields.py | 22 +++--- .../natural_source/receivers.py | 8 +- .../natural_source/simulation.py | 0 .../natural_source/simulation_1d.py | 0 .../natural_source/sources.py | 14 ++-- .../electromagnetics/natural_source/survey.py | 8 +- .../natural_source/utils/__init__.py | 2 +- .../natural_source/utils/analytic_1d.py | 0 .../natural_source/utils/data_utils.py | 10 +-- .../natural_source/utils/data_viewer.py | 0 .../natural_source/utils/edi_files_utils.py | 2 +- .../natural_source/utils/plot_data_types.py | 0 .../natural_source/utils/plot_utils.py | 0 .../natural_source/utils/solutions_1d.py | 0 .../natural_source/utils/source_utils.py | 2 +- .../natural_source/utils/test_utils.py | 2 +- .../electromagnetics/static/__init__.py | 0 .../static/induced_polarization/__init__.py | 6 +- .../static/induced_polarization/run.py | 2 +- .../static/induced_polarization/simulation.py | 0 .../static/induced_polarization/survey.py | 0 .../static/resistivity/IODC.py | 0 .../static/resistivity/__init__.py | 4 +- .../static/resistivity/fields.py | 0 .../static/resistivity/fields_2d.py | 0 .../static/resistivity/receivers.py | 4 +- .../static/resistivity/run.py | 2 +- .../static/resistivity/simulation.py | 0 .../static/resistivity/simulation_1d.py | 0 .../static/resistivity/simulation_2d.py | 2 +- .../static/resistivity/sources.py | 0 .../static/resistivity/survey.py | 4 +- .../static/resistivity/utils.py | 4 +- .../spectral_induced_polarization/__init__.py | 4 +- .../spectral_induced_polarization/data.py | 0 .../receivers.py | 4 +- .../spectral_induced_polarization/run.py | 2 +- .../simulation.py | 0 .../simulation_2d.py | 0 .../spectral_induced_polarization/sources.py | 4 +- .../spectral_induced_polarization/survey.py | 2 +- .../static/spontaneous_potential/__init__.py | 6 +- .../spontaneous_potential/simulation.py | 2 +- .../static/spontaneous_potential/sources.py | 6 +- .../electromagnetics/static/utils/__init__.py | 4 +- .../static/utils/static_utils.py | 14 ++-- .../electromagnetics/time_domain/__init__.py | 4 +- .../electromagnetics/time_domain/fields.py | 0 .../electromagnetics/time_domain/receivers.py | 20 ++--- .../time_domain/simulation.py | 6 +- .../time_domain/simulation_1d.py | 0 .../electromagnetics/time_domain/sources.py | 30 ++++---- .../electromagnetics/time_domain/survey.py | 2 +- .../electromagnetics/utils/__init__.py | 4 +- .../electromagnetics/utils/current_utils.py | 0 .../electromagnetics/utils/em1d_utils.py | 2 +- .../electromagnetics/utils/testing_utils.py | 4 +- .../electromagnetics/utils/waveform_utils.py | 2 +- .../__init__.py | 4 +- .../receivers.py | 0 .../simulation.py | 2 +- .../viscous_remanent_magnetization/sources.py | 2 +- .../viscous_remanent_magnetization/survey.py | 2 +- .../waveforms.py | 0 {SimPEG => simpeg}/fields.py | 4 +- {SimPEG => simpeg}/flow/__init__.py | 0 {SimPEG => simpeg}/flow/richards/__init__.py | 4 +- {SimPEG => simpeg}/flow/richards/empirical.py | 4 +- {SimPEG => simpeg}/flow/richards/receivers.py | 4 +- .../flow/richards/simulation.py | 0 {SimPEG => simpeg}/flow/richards/survey.py | 6 +- {SimPEG => simpeg}/inverse_problem.py | 16 ++-- {SimPEG => simpeg}/inversion.py | 4 +- {SimPEG => simpeg}/maps.py | 36 ++++----- {SimPEG => simpeg}/meta/__init__.py | 4 +- {SimPEG => simpeg}/meta/dask_sim.py | 26 +++---- {SimPEG => simpeg}/meta/multiprocessing.py | 12 +-- {SimPEG => simpeg}/meta/simulation.py | 16 ++-- {SimPEG => simpeg}/models.py | 2 +- {SimPEG => simpeg}/objective_function.py | 22 +++--- {SimPEG => simpeg}/optimization.py | 8 +- .../potential_fields/__init__.py | 4 +- {SimPEG => simpeg}/potential_fields/base.py | 4 +- .../potential_fields/gravity/__init__.py | 4 +- .../gravity/_numba_functions.py | 0 .../potential_fields/gravity/analytics.py | 2 +- .../potential_fields/gravity/receivers.py | 2 +- .../potential_fields/gravity/simulation.py | 4 +- .../potential_fields/gravity/sources.py | 2 +- .../potential_fields/gravity/survey.py | 0 .../potential_fields/magnetics/__init__.py | 4 +- .../potential_fields/magnetics/analytics.py | 4 +- .../potential_fields/magnetics/receivers.py | 0 .../potential_fields/magnetics/simulation.py | 10 +-- .../potential_fields/magnetics/sources.py | 8 +- .../potential_fields/magnetics/survey.py | 4 +- {SimPEG => simpeg}/props.py | 2 +- {SimPEG => simpeg}/regularization/__init__.py | 4 +- .../regularization/_gradient.py | 2 +- {SimPEG => simpeg}/regularization/base.py | 40 +++++----- .../regularization/correspondence.py | 2 +- .../regularization/cross_gradient.py | 2 +- {SimPEG => simpeg}/regularization/jtv.py | 2 +- {SimPEG => simpeg}/regularization/pgi.py | 22 +++--- .../regularization/regularization_mesh.py | 4 +- {SimPEG => simpeg}/regularization/sparse.py | 12 +-- {SimPEG => simpeg}/regularization/vector.py | 10 +-- {SimPEG => simpeg}/seismic/__init__.py | 0 .../straight_ray_tomography/__init__.py | 4 +- .../straight_ray_tomography/simulation.py | 0 .../seismic/straight_ray_tomography/survey.py | 0 {SimPEG => simpeg}/simulation.py | 30 ++++---- {SimPEG => simpeg}/survey.py | 14 ++-- {SimPEG => simpeg}/utils/__init__.py | 4 +- {SimPEG => simpeg}/utils/code_utils.py | 8 +- {SimPEG => simpeg}/utils/coord_utils.py | 0 {SimPEG => simpeg}/utils/counter_utils.py | 2 +- {SimPEG => simpeg}/utils/curv_utils.py | 0 {SimPEG => simpeg}/utils/drivers/__init__.py | 0 .../utils/drivers/gravity_driver.py | 2 +- .../utils/drivers/magnetics_driver.py | 4 +- {SimPEG => simpeg}/utils/io_utils/__init__.py | 0 .../io_utils/io_utils_electromagnetics.py | 38 +++++----- .../utils/io_utils/io_utils_general.py | 0 .../utils/io_utils/io_utils_pf.py | 28 +++---- {SimPEG => simpeg}/utils/mat_utils.py | 8 +- {SimPEG => simpeg}/utils/mesh_utils.py | 0 {SimPEG => simpeg}/utils/model_builder.py | 2 +- {SimPEG => simpeg}/utils/model_utils.py | 0 {SimPEG => simpeg}/utils/pgi_utils.py | 2 +- {SimPEG => simpeg}/utils/plot_utils.py | 0 {SimPEG => simpeg}/utils/solver_utils.py | 6 +- .../regularizations/test_cross_gradient.py | 2 +- .../regularizations/test_full_gradient.py | 2 +- tests/base/regularizations/test_jtv.py | 2 +- .../test_pgi_regularization.py | 6 +- .../regularizations/test_regularization.py | 6 +- tests/base/test_Fields.py | 2 +- tests/base/test_Props.py | 6 +- tests/base/test_Solver.py | 4 +- tests/base/test_coordutils.py | 2 +- tests/base/test_correspondance.py | 2 +- tests/base/test_data.py | 6 +- tests/base/test_data_misfit.py | 4 +- tests/base/test_directives.py | 4 +- tests/base/test_joint.py | 4 +- tests/base/test_maps.py | 2 +- tests/base/test_mass_matrices.py | 4 +- tests/base/test_model_utils.py | 2 +- tests/base/test_objective_function.py | 8 +- tests/base/test_optimizers.py | 4 +- tests/base/test_problem.py | 2 +- tests/base/test_simulation.py | 2 +- tests/base/test_stub.py | 14 ++++ tests/base/test_survey_data.py | 2 +- tests/base/test_utils.py | 2 +- tests/base/test_validators.py | 2 +- tests/dask/test_DC_jvecjtvecadj_dask.py | 8 +- tests/dask/test_IP_jvecjtvecadj_dask.py | 10 +-- tests/dask/test_grav_inversion_linear.py | 6 +- tests/dask/test_mag_MVI_Octree.py | 6 +- .../dask/test_mag_inversion_linear_Octree.py | 6 +- tests/dask/test_mag_nonLinear_Amplitude.py | 12 +-- tests/em/em1d/test_EM1D_FD_fwd.py | 4 +- tests/em/em1d/test_EM1D_FD_jac_layers.py | 4 +- tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py | 6 +- tests/em/em1d/test_EM1D_TD_general_fwd.py | 6 +- .../em1d/test_EM1D_TD_general_jac_layers.py | 4 +- tests/em/em1d/test_EM1D_TD_off_fwd.py | 4 +- tests/em/em1d/test_EM1D_TD_off_jac_layers.py | 4 +- tests/em/fdem/forward/test_FDEM_analytics.py | 6 +- tests/em/fdem/forward/test_FDEM_casing.py | 4 +- tests/em/fdem/forward/test_FDEM_forward.py | 6 +- .../em/fdem/forward/test_FDEM_forwardEJHB.py | 2 +- tests/em/fdem/forward/test_FDEM_forwardHB.py | 2 +- tests/em/fdem/forward/test_FDEM_primsec.py | 4 +- tests/em/fdem/forward/test_FDEM_sources.py | 4 +- tests/em/fdem/forward/test_permittivity.py | 2 +- tests/em/fdem/forward/test_properties.py | 4 +- .../inverse/adjoint/test_FDEM_adjointEB.py | 2 +- .../inverse/adjoint/test_FDEM_adjointHJ.py | 2 +- .../fdem/inverse/derivs/test_FDEM_derivs.py | 4 +- tests/em/fdem/muinverse/test_muinverse.py | 4 +- .../em/nsem/forward/test_1D_finite_volume.py | 4 +- .../test_AnalyticFunctionVsAppResPhs.py | 4 +- .../test_Problem1D_AnalyticVsNumeric.py | 4 +- .../test_Problem1D_VsAnalyticHalfspace.py | 2 +- .../test_Problem3D_VsAnalyticSolution.py | 2 +- .../test_Recursive1D_VsAnalyticHalfspace.py | 4 +- tests/em/nsem/inversion/test_BC_Sims.py | 4 +- .../nsem/inversion/test_Problem1D_Adjoint.py | 4 +- .../nsem/inversion/test_Problem1D_Derivs.py | 4 +- .../nsem/inversion/test_Problem3D_Adjoint.py | 2 +- .../nsem/inversion/test_Problem3D_Derivs.py | 4 +- .../inversion/test_complex_resistivity.py | 6 +- tests/em/nsem/survey/test_nsem_data.py | 2 +- tests/em/nsem/utils/test_data_utils.py | 2 +- tests/em/static/test_DCIP_io_utils.py | 8 +- tests/em/static/test_DC_1D_jvecjtvecadj.py | 4 +- tests/em/static/test_DC_2D_analytic.py | 6 +- tests/em/static/test_DC_2D_jvecjtvecadj.py | 6 +- tests/em/static/test_DC_Boundaries.py | 2 +- .../static/test_DC_FieldsDipoleFullspace.py | 6 +- .../test_DC_FieldsMultipoleFullspace.py | 6 +- tests/em/static/test_DC_Utils.py | 10 +-- tests/em/static/test_DC_analytic.py | 8 +- tests/em/static/test_DC_jvecjtvecadj.py | 6 +- tests/em/static/test_DC_miniaturize.py | 6 +- tests/em/static/test_IO.py | 4 +- tests/em/static/test_IP_2D_fwd.py | 8 +- tests/em/static/test_IP_2D_jvecjtvecadj.py | 22 +++--- tests/em/static/test_IP_fwd.py | 8 +- tests/em/static/test_IP_jvecjtvecadj.py | 20 ++--- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 6 +- tests/em/static/test_SIP_jvecjtvecadj.py | 6 +- tests/em/static/test_SPjvecjtvecadj.py | 8 +- tests/em/static/test_properties.py | 4 +- tests/em/tdem/test_TDEM_DerivAdjoint.py | 4 +- .../test_TDEM_DerivAdjoint_RawWaveform.py | 6 +- .../tdem/test_TDEM_DerivAdjoint_galvanic.py | 4 +- tests/em/tdem/test_TDEM_crosscheck.py | 6 +- tests/em/tdem/test_TDEM_forward_Analytic.py | 6 +- .../test_TDEM_forward_Analytic_RawWaveform.py | 8 +- tests/em/tdem/test_TDEM_grounded.py | 6 +- .../em/tdem/test_TDEM_inductive_permeable.py | 4 +- tests/em/tdem/test_TDEM_sources.py | 2 +- tests/em/tdem/test_properties.py | 2 +- tests/em/utils/test_linecurrents.py | 4 +- tests/em/vrm/test_vrmfwd.py | 2 +- tests/em/vrm/test_vrminv.py | 16 ++-- tests/em/vrm/test_waveform.py | 2 +- tests/flow/test_Richards.py | 8 +- tests/flow/test_Richards_empirical.py | 4 +- tests/meta/test_dask_meta.py | 8 +- tests/meta/test_meta_sim.py | 8 +- tests/meta/test_multiprocessing_sim.py | 8 +- tests/pf/test_forward_Grav_Linear.py | 10 +-- tests/pf/test_forward_Mag_Linear.py | 4 +- tests/pf/test_forward_PFproblem.py | 6 +- tests/pf/test_grav_inversion_linear.py | 4 +- tests/pf/test_gravity_IO.py | 6 +- tests/pf/test_mag_MVI_Octree.py | 4 +- tests/pf/test_mag_inversion_linear.py | 6 +- tests/pf/test_mag_inversion_linear_Octree.py | 4 +- tests/pf/test_mag_nonLinear_Amplitude.py | 10 +-- tests/pf/test_mag_uniform_background_field.py | 2 +- tests/pf/test_mag_vector_amplitude.py | 4 +- tests/pf/test_magnetics_IO.py | 6 +- tests/pf/test_magnetics_analytics.py | 6 +- tests/pf/test_pf_quadtree_inversion_linear.py | 4 +- tests/pf/test_sensitivity_PFproblem.py | 6 +- tests/pf/test_survey_counting.py | 4 +- tests/seis/test_tomo.py | 4 +- tests/utils/test_deprecate.py | 76 +++++++++---------- tests/utils/test_gmm_utils.py | 4 +- tests/utils/test_io_utils.py | 8 +- tests/utils/test_mat_utils.py | 12 +-- tests/utils/test_report.py | 4 +- tests/utils/test_solverwrap.py | 2 +- .../01-models_mapping/plot_1_tensor_models.py | 4 +- .../01-models_mapping/plot_2_cyl_models.py | 4 +- .../01-models_mapping/plot_3_tree_models.py | 4 +- .../plot_inv_1_inversion_lsq.py | 2 +- .../plot_inv_2_inversion_irls.py | 2 +- .../03-gravity/plot_1a_gravity_anomaly.py | 8 +- .../03-gravity/plot_1b_gravity_gradiometry.py | 8 +- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 6 +- .../plot_inv_1b_gravity_anomaly_irls.py | 6 +- .../04-magnetics/plot_2a_magnetics_induced.py | 8 +- .../04-magnetics/plot_2b_magnetics_mvi.py | 8 +- .../plot_inv_2a_magnetics_induced.py | 6 +- tutorials/05-dcr/plot_fwd_1_dcr_sounding.py | 8 +- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 14 ++-- tutorials/05-dcr/plot_fwd_3_dcr3d.py | 16 ++-- tutorials/05-dcr/plot_gen_3_3d_to_2d.py | 8 +- tutorials/05-dcr/plot_inv_1_dcr_sounding.py | 8 +- .../05-dcr/plot_inv_1_dcr_sounding_irls.py | 8 +- .../plot_inv_1_dcr_sounding_parametric.py | 8 +- tutorials/05-dcr/plot_inv_2_dcr2d.py | 12 +-- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 12 +-- tutorials/05-dcr/plot_inv_3_dcr3d.py | 14 ++-- tutorials/06-ip/plot_fwd_2_dcip2d.py | 18 ++--- tutorials/06-ip/plot_fwd_3_dcip3d.py | 20 ++--- tutorials/06-ip/plot_inv_2_dcip2d.py | 14 ++-- tutorials/06-ip/plot_inv_3_dcip3d.py | 16 ++-- tutorials/07-fdem/plot_fwd_1_em1dfm.py | 10 +-- .../07-fdem/plot_fwd_1_em1dfm_dispersive.py | 10 +-- tutorials/07-fdem/plot_fwd_2_fem_cyl.py | 8 +- tutorials/07-fdem/plot_fwd_3_fem_3d.py | 10 +-- tutorials/07-fdem/plot_inv_1_em1dfm.py | 8 +- tutorials/08-tdem/plot_fwd_1_em1dtm.py | 10 +-- .../08-tdem/plot_fwd_1_em1dtm_dispersive.py | 10 +-- .../08-tdem/plot_fwd_1_em1dtm_waveforms.py | 6 +- tutorials/08-tdem/plot_fwd_2_tem_cyl.py | 10 +-- tutorials/08-tdem/plot_fwd_3_tem_3d.py | 12 +-- tutorials/08-tdem/plot_inv_1_em1dtm.py | 8 +- tutorials/10-vrm/plot_fwd_1_vrm_layer.py | 6 +- tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py | 8 +- tutorials/10-vrm/plot_fwd_3_vrm_tem.py | 12 +-- .../12-seismic/plot_fwd_1_tomography_2D.py | 8 +- .../12-seismic/plot_inv_1_tomography_2D.py | 4 +- .../plot_inv_3_cross_gradient_pf.py | 6 +- ...t_inv_1_joint_pf_pgi_full_info_tutorial.py | 6 +- ...lot_inv_2_joint_pf_pgi_no_info_tutorial.py | 6 +- .../_temporary/plot_4c_fdem3d_inversion.py | 8 +- .../plot_fwd_1_em1dtm_stitched_skytem.py | 10 +-- .../plot_inv_1_em1dtm_stitched_skytem.py | 10 +-- 415 files changed, 1264 insertions(+), 1238 deletions(-) create mode 100644 SimPEG.py delete mode 100644 SimPEG/dask/__init__.py rename {SimPEG => simpeg}/__init__.py (96%) rename {SimPEG => simpeg}/base/__init__.py (100%) rename {SimPEG => simpeg}/base/pde_simulation.py (100%) create mode 100644 simpeg/dask/__init__.py rename {SimPEG => simpeg}/dask/electromagnetics/__init__.py (100%) rename {SimPEG => simpeg}/dask/electromagnetics/static/__init__.py (100%) rename {SimPEG => simpeg}/dask/electromagnetics/static/induced_polarization/__init__.py (100%) rename {SimPEG => simpeg}/dask/electromagnetics/static/induced_polarization/simulation.py (100%) rename {SimPEG => simpeg}/dask/electromagnetics/static/resistivity/__init__.py (100%) rename {SimPEG => simpeg}/dask/electromagnetics/static/resistivity/simulation.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/__init__.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/base.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/gravity/__init__.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/gravity/simulation.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/magnetics/__init__.py (100%) rename {SimPEG => simpeg}/dask/potential_fields/magnetics/simulation.py (100%) rename {SimPEG => simpeg}/dask/simulation.py (100%) rename {SimPEG => simpeg}/dask/utils.py (100%) rename {SimPEG => simpeg}/data.py (98%) rename {SimPEG => simpeg}/data_misfit.py (95%) rename {SimPEG => simpeg}/directives/__init__.py (94%) rename {SimPEG => simpeg}/directives/directives.py (98%) rename {SimPEG => simpeg}/directives/pgi_directives.py (98%) rename {SimPEG => simpeg}/directives/sim_directives.py (100%) rename {SimPEG => simpeg}/electromagnetics/__init__.py (92%) rename {SimPEG => simpeg}/electromagnetics/analytics/DC.py (100%) rename {SimPEG => simpeg}/electromagnetics/analytics/FDEM.py (98%) rename {SimPEG => simpeg}/electromagnetics/analytics/FDEMDipolarfields.py (99%) rename {SimPEG => simpeg}/electromagnetics/analytics/FDEMcasing.py (99%) rename {SimPEG => simpeg}/electromagnetics/analytics/NSEM.py (99%) rename {SimPEG => simpeg}/electromagnetics/analytics/TDEM.py (99%) rename {SimPEG => simpeg}/electromagnetics/analytics/__init__.py (100%) rename {SimPEG => simpeg}/electromagnetics/base.py (97%) rename {SimPEG => simpeg}/electromagnetics/base_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/__init__.py (93%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/fields.py (98%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/receivers.py (97%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/simulation.py (98%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/simulation_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/sources.py (98%) rename {SimPEG => simpeg}/electromagnetics/frequency_domain/survey.py (97%) rename {SimPEG => simpeg}/electromagnetics/natural_source/__init__.py (92%) rename {SimPEG => simpeg}/electromagnetics/natural_source/fields.py (97%) rename {SimPEG => simpeg}/electromagnetics/natural_source/receivers.py (98%) rename {SimPEG => simpeg}/electromagnetics/natural_source/simulation.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/simulation_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/sources.py (93%) rename {SimPEG => simpeg}/electromagnetics/natural_source/survey.py (97%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/__init__.py (95%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/analytic_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/data_utils.py (98%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/data_viewer.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/edi_files_utils.py (99%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/plot_data_types.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/plot_utils.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/solutions_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/source_utils.py (99%) rename {SimPEG => simpeg}/electromagnetics/natural_source/utils/test_utils.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/__init__.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/induced_polarization/__init__.py (83%) rename {SimPEG => simpeg}/electromagnetics/static/induced_polarization/run.py (98%) rename {SimPEG => simpeg}/electromagnetics/static/induced_polarization/simulation.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/induced_polarization/survey.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/IODC.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/__init__.py (93%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/fields.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/fields_2d.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/receivers.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/run.py (98%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/simulation.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/simulation_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/simulation_2d.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/sources.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/survey.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/resistivity/utils.py (96%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/__init__.py (91%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/data.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/receivers.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/run.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/simulation.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/simulation_2d.py (100%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/sources.py (98%) rename {SimPEG => simpeg}/electromagnetics/static/spectral_induced_polarization/survey.py (98%) rename {SimPEG => simpeg}/electromagnetics/static/spontaneous_potential/__init__.py (82%) rename {SimPEG => simpeg}/electromagnetics/static/spontaneous_potential/simulation.py (99%) rename {SimPEG => simpeg}/electromagnetics/static/spontaneous_potential/sources.py (85%) rename {SimPEG => simpeg}/electromagnetics/static/utils/__init__.py (92%) rename {SimPEG => simpeg}/electromagnetics/static/utils/static_utils.py (99%) rename {SimPEG => simpeg}/electromagnetics/time_domain/__init__.py (94%) rename {SimPEG => simpeg}/electromagnetics/time_domain/fields.py (100%) rename {SimPEG => simpeg}/electromagnetics/time_domain/receivers.py (94%) rename {SimPEG => simpeg}/electromagnetics/time_domain/simulation.py (99%) rename {SimPEG => simpeg}/electromagnetics/time_domain/simulation_1d.py (100%) rename {SimPEG => simpeg}/electromagnetics/time_domain/sources.py (98%) rename {SimPEG => simpeg}/electromagnetics/time_domain/survey.py (93%) rename {SimPEG => simpeg}/electromagnetics/utils/__init__.py (87%) rename {SimPEG => simpeg}/electromagnetics/utils/current_utils.py (100%) rename {SimPEG => simpeg}/electromagnetics/utils/em1d_utils.py (99%) rename {SimPEG => simpeg}/electromagnetics/utils/testing_utils.py (98%) rename {SimPEG => simpeg}/electromagnetics/utils/waveform_utils.py (98%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/__init__.py (91%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/receivers.py (100%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/simulation.py (99%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/sources.py (99%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/survey.py (97%) rename {SimPEG => simpeg}/electromagnetics/viscous_remanent_magnetization/waveforms.py (100%) rename {SimPEG => simpeg}/fields.py (99%) rename {SimPEG => simpeg}/flow/__init__.py (100%) rename {SimPEG => simpeg}/flow/richards/__init__.py (92%) rename {SimPEG => simpeg}/flow/richards/empirical.py (99%) rename {SimPEG => simpeg}/flow/richards/receivers.py (96%) rename {SimPEG => simpeg}/flow/richards/simulation.py (100%) rename {SimPEG => simpeg}/flow/richards/survey.py (93%) rename {SimPEG => simpeg}/inverse_problem.py (95%) rename {SimPEG => simpeg}/inversion.py (96%) rename {SimPEG => simpeg}/maps.py (99%) rename {SimPEG => simpeg}/meta/__init__.py (96%) rename {SimPEG => simpeg}/meta/dask_sim.py (96%) rename {SimPEG => simpeg}/meta/multiprocessing.py (98%) rename {SimPEG => simpeg}/meta/simulation.py (97%) rename {SimPEG => simpeg}/models.py (98%) rename {SimPEG => simpeg}/objective_function.py (97%) rename {SimPEG => simpeg}/optimization.py (99%) rename {SimPEG => simpeg}/potential_fields/__init__.py (81%) rename {SimPEG => simpeg}/potential_fields/base.py (99%) rename {SimPEG => simpeg}/potential_fields/gravity/__init__.py (89%) rename {SimPEG => simpeg}/potential_fields/gravity/_numba_functions.py (100%) rename {SimPEG => simpeg}/potential_fields/gravity/analytics.py (98%) rename {SimPEG => simpeg}/potential_fields/gravity/receivers.py (97%) rename {SimPEG => simpeg}/potential_fields/gravity/simulation.py (99%) rename {SimPEG => simpeg}/potential_fields/gravity/sources.py (83%) rename {SimPEG => simpeg}/potential_fields/gravity/survey.py (100%) rename {SimPEG => simpeg}/potential_fields/magnetics/__init__.py (89%) rename {SimPEG => simpeg}/potential_fields/magnetics/analytics.py (99%) rename {SimPEG => simpeg}/potential_fields/magnetics/receivers.py (100%) rename {SimPEG => simpeg}/potential_fields/magnetics/simulation.py (99%) rename {SimPEG => simpeg}/potential_fields/magnetics/sources.py (93%) rename {SimPEG => simpeg}/potential_fields/magnetics/survey.py (95%) rename {SimPEG => simpeg}/props.py (99%) rename {SimPEG => simpeg}/regularization/__init__.py (99%) rename {SimPEG => simpeg}/regularization/_gradient.py (99%) rename {SimPEG => simpeg}/regularization/base.py (98%) rename {SimPEG => simpeg}/regularization/correspondence.py (99%) rename {SimPEG => simpeg}/regularization/cross_gradient.py (99%) rename {SimPEG => simpeg}/regularization/jtv.py (99%) rename {SimPEG => simpeg}/regularization/pgi.py (99%) rename {SimPEG => simpeg}/regularization/regularization_mesh.py (99%) rename {SimPEG => simpeg}/regularization/sparse.py (99%) rename {SimPEG => simpeg}/regularization/vector.py (99%) rename {SimPEG => simpeg}/seismic/__init__.py (100%) rename {SimPEG => simpeg}/seismic/straight_ray_tomography/__init__.py (83%) rename {SimPEG => simpeg}/seismic/straight_ray_tomography/simulation.py (100%) rename {SimPEG => simpeg}/seismic/straight_ray_tomography/survey.py (100%) rename {SimPEG => simpeg}/simulation.py (98%) rename {SimPEG => simpeg}/survey.py (98%) rename {SimPEG => simpeg}/utils/__init__.py (98%) rename {SimPEG => simpeg}/utils/code_utils.py (99%) rename {SimPEG => simpeg}/utils/coord_utils.py (100%) rename {SimPEG => simpeg}/utils/counter_utils.py (98%) rename {SimPEG => simpeg}/utils/curv_utils.py (100%) rename {SimPEG => simpeg}/utils/drivers/__init__.py (100%) rename {SimPEG => simpeg}/utils/drivers/gravity_driver.py (99%) rename {SimPEG => simpeg}/utils/drivers/magnetics_driver.py (99%) rename {SimPEG => simpeg}/utils/io_utils/__init__.py (100%) rename {SimPEG => simpeg}/utils/io_utils/io_utils_electromagnetics.py (96%) rename {SimPEG => simpeg}/utils/io_utils/io_utils_general.py (100%) rename {SimPEG => simpeg}/utils/io_utils/io_utils_pf.py (94%) rename {SimPEG => simpeg}/utils/mat_utils.py (98%) rename {SimPEG => simpeg}/utils/mesh_utils.py (100%) rename {SimPEG => simpeg}/utils/model_builder.py (99%) rename {SimPEG => simpeg}/utils/model_utils.py (100%) rename {SimPEG => simpeg}/utils/pgi_utils.py (99%) rename {SimPEG => simpeg}/utils/plot_utils.py (100%) rename {SimPEG => simpeg}/utils/solver_utils.py (98%) create mode 100644 tests/base/test_stub.py diff --git a/.coveragerc b/.coveragerc index 8cb6275aa3..a79c8987f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] -source = SimPEG +source = simpeg omit = */setup.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index ba93c01385..130f534ba2 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -25,7 +25,7 @@ body: bug or missing functionality. It should be able to be copy-pasted into a Python interpreter and run as-is. placeholder: | - import SimPEG + import simpeg << your code here >> render: python validations: @@ -44,8 +44,8 @@ body: attributes: label: "Runtime information:" description: > - Please include the output from `SimPEG.Report()` to describe your system for us. - Paste the output from `from SimPEG import Report; print(Report())` below. + Please include the output from `simpeg.Report()` to describe your system for us. + Paste the output from `from simpeg import Report; print(Report())` below. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/post-install.yml b/.github/ISSUE_TEMPLATE/post-install.yml index 6abd27d75a..10b970aebf 100644 --- a/.github/ISSUE_TEMPLATE/post-install.yml +++ b/.github/ISSUE_TEMPLATE/post-install.yml @@ -1,5 +1,5 @@ name: Post-install/importing issue -description: Report an issue if you have trouble importing SimPEG after installation. +description: Report an issue if you have trouble importing `simpeg` after installation. title: "" labels: [Installation] diff --git a/.gitignore b/.gitignore index d85d053a89..c274ffb66f 100644 --- a/.gitignore +++ b/.gitignore @@ -92,4 +92,4 @@ tutorials/13-joint_inversion/cross_gradient_data/* *.tar.gz # setuptools_scm -SimPEG/version.py +simpeg/version.py diff --git a/Makefile b/Makefile index c4981ef69f..20c18b456c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -STYLE_CHECK_FILES = SimPEG examples tutorials tests +STYLE_CHECK_FILES = simpeg examples tutorials tests .PHONY: help build coverage lint graphs tests docs check black flake @@ -15,14 +15,14 @@ build: python setup.py build_ext --inplace coverage: - nosetests --logging-level=INFO --with-coverage --cover-package=SimPEG --cover-html + nosetests --logging-level=INFO --with-coverage --cover-package=simpeg --cover-html open cover/index.html lint: - pylint --output-format=html SimPEG > pylint.html + pylint --output-format=html simpeg> pylint.html graphs: - pyreverse -my -A -o pdf -p SimPEG SimPEG/**.py SimPEG/**/**.py + pyreverse -my -A -o pdf -p simpeg simpeg/**.py simpeg/**/**.py tests: nosetests --logging-level=INFO diff --git a/README.rst b/README.rst index 7545b24db7..54041d24e3 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,15 @@ .. image:: https://raw.github.com/simpeg/simpeg/main/docs/images/simpeg-logo.png - :alt: SimPEG Logo + :alt: simpeg Logo SimPEG ****** -.. image:: https://img.shields.io/pypi/v/SimPEG.svg - :target: https://pypi.python.org/pypi/SimPEG +.. image:: https://img.shields.io/pypi/v/simpeg.svg + :target: https://pypi.python.org/pypi/simpeg :alt: Latest PyPI version -.. image:: https://img.shields.io/conda/v/conda-forge/SimPEG.svg - :target: https://anaconda.org/conda-forge/SimPEG +.. image:: https://img.shields.io/conda/v/conda-forge/simpeg.svg + :target: https://anaconda.org/conda-forge/simpeg :alt: Latest conda-forge version .. image:: https://img.shields.io/github/license/simpeg/simpeg.svg diff --git a/SimPEG.py b/SimPEG.py new file mode 100644 index 0000000000..c923431ba0 --- /dev/null +++ b/SimPEG.py @@ -0,0 +1,11 @@ +import sys +import warnings + +warnings.warn( + "Importing `SimPEG` is deprecated. please import from `simpeg`.", + FutureWarning, + stacklevel=2, +) +import simpeg + +sys.modules["SimPEG"] = simpeg diff --git a/SimPEG/dask/__init__.py b/SimPEG/dask/__init__.py deleted file mode 100644 index dff45afbfb..0000000000 --- a/SimPEG/dask/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -try: - import SimPEG.dask.simulation - import SimPEG.dask.electromagnetics.static.resistivity.simulation - import SimPEG.dask.electromagnetics.static.induced_polarization.simulation - import SimPEG.dask.potential_fields.base - import SimPEG.dask.potential_fields.gravity.simulation - import SimPEG.dask.potential_fields.magnetics.simulation -except ImportError as err: - print("unable to load dask operations") - print(err) diff --git a/docs/Makefile b/docs/Makefile index 47d100db07..3751f949e1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,17 +87,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SimPEG.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/simpeg.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SimPEG.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/simpeg.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/SimPEG" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SimPEG" + @echo "# mkdir -p $$HOME/.local/share/devhelp/simpeg" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/simpeg" @echo "# devhelp" epub: diff --git a/docs/README.md b/docs/README.md index df4d1fb872..5dd5d195fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,5 +4,5 @@ A place where we automatically deploy the SimPEG documentation. This repository is automatically edited by our army of robots on the Azure CI service. Changes to the documentation should not be made -here but rather in the [`simpeg`](https://github.com/simpeg/simpeg) +here but rather in the [SimPEG](https://github.com/simpeg/simpeg) repository. diff --git a/docs/conf.py b/docs/conf.py index b7edd86caf..dde265d6d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ import os from sphinx_gallery.sorting import FileNameSortKey import glob -import SimPEG +import simpeg import plotly.io as pio from importlib.metadata import version @@ -78,7 +78,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = version("SimPEG") +release = version("simpeg") # The short X.Y version. version = ".".join(release.split(".")[:2]) @@ -188,11 +188,11 @@ def linkcode_resolve(domain, info): linespec = "" try: - fn = relpath(fn, start=dirname(SimPEG.__file__)) + fn = relpath(fn, start=dirname(simpeg.__file__)) except ValueError: return None - return f"https://github.com/simpeg/simpeg/blob/main/SimPEG/{fn}{linespec}" + return f"https://github.com/simpeg/simpeg/blob/main/simpeg/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") @@ -365,7 +365,7 @@ def linkcode_resolve(domain, info): # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = "SimPEGdoc" +htmlhelp_basename = "simpegdoc" # -- Options for LaTeX output -------------------------------------------------- @@ -382,7 +382,7 @@ def linkcode_resolve(domain, info): # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "SimPEG.tex", "SimPEG Documentation", "SimPEG Team", "manual"), + ("index", "simpeg.tex", "SimPEG Documentation", "SimPEG Team", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -410,7 +410,7 @@ def linkcode_resolve(domain, info): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "simpeg", "SimPEG Documentation", ["SimPEG Team"], 1)] +man_pages = [("index", "SimPEG", "SimPEG Documentation", ["SimPEG Team"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -466,7 +466,7 @@ def linkcode_resolve(domain, info): "within_subsection_order": FileNameSortKey, "filename_pattern": "\.py", "backreferences_dir": "content/api/generated/backreferences", - "doc_module": "SimPEG", + "doc_module": "simpeg", "show_memory": True, "image_scrapers": image_scrapers, } diff --git a/docs/content/release/0.14.0-notes.rst b/docs/content/release/0.14.0-notes.rst index dfb8949f38..1c4aa36702 100644 --- a/docs/content/release/0.14.0-notes.rst +++ b/docs/content/release/0.14.0-notes.rst @@ -92,7 +92,7 @@ easier to interface with external codes for inversion purposes, as all that is needed to be defined to use a ``Simulation`` for an ``InvProblem``, is ``sim.dpred``, ``sim.Jvec`` and ``sim.Jtvec``. -Please see the documentation for the :class:`SimPEG.simulation.BaseSimulation` class +Please see the documentation for the :class:`simpeg.simulation.BaseSimulation` class as well as the individual methods' ``Simulation``-s, for a detailed description of arguments, but largely it accepts the same arguments as the ``Problem`` class, but now also requires a ``Survey`` to be set. @@ -105,9 +105,9 @@ data, we are concerned with the data. Thus we would like to enforce this importa by making data live in a dedicated ``Data`` class. This ``Data`` class can act like a smart dictionary to grab data associated with a specific source, receiver combination. More importantly, this ``Data`` class is where we store information related to observed -data and its errors. This class started in the ``SimPEG.Survey`` module, but has -now been moved into its own new module ``SimPEG.data``. See the documentation for -the :class:`SimPEG.data.Data` for all of the details. +data and its errors. This class started in the ``simpeg.Survey`` module, but has +now been moved into its own new module ``simpeg.data``. See the documentation for +the :class:`simpeg.data.Data` for all of the details. Previously, @@ -179,7 +179,7 @@ the definition of the classic data misfit measure. The ``Simulation`` class handles the forward operation, :math:`\mathcal{F}`, and the ``Data`` class handles the noise, :math:`\textbf{W}_d=diag(\frac{1}{\sigma_i})`, and the observed data, :math:`\vec{d}_{obs}`. See the documentation for the -:class:`SimPEG.data_misfit.L2DataMisfit` for all of the details. +:class:`simpeg.data_misfit.L2DataMisfit` for all of the details. Previously, @@ -210,18 +210,18 @@ This feature is experimental at the moment and can be toggled on like so, .. code-block:: python - import SimPEG.dask + import simpeg.dask which will then enable parallel operations for a few modules. It will specifically replace these functions with ``dask`` versions, -* ``SimPEG.potential_fields.BasePFSimulation.linear_operator`` -* ``SimPEG.potential_fields.magnetics.Simulation3DIntegral.getJtJdiag`` -* ``SimPEG.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`` -* ``SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJ`` -* ``SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJtJdiag`` -* ``SimPEG.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJ`` -* ``SimPEG.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJtJdiag`` +* ``simpeg.potential_fields.BasePFSimulation.linear_operator`` +* ``simpeg.potential_fields.magnetics.Simulation3DIntegral.getJtJdiag`` +* ``simpeg.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`` +* ``simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJ`` +* ``simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJtJdiag`` +* ``simpeg.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJ`` +* ``simpeg.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJtJdiag`` Changelog ========= @@ -317,19 +317,19 @@ moved around and been renamed. There are now two separate modules within ``poten ``gravity`` and ``magnetics``. All of the classes in ``PF.BaseGrav`` have been moved to ``potential_fields.gravity``, and the classes in ``PF.BaseMag`` have been moved to ``potential_fields.magnetics``. The ``Map``-s that were within them have -been deprecated and can instead be found in ``SimPEG.maps``. +been deprecated and can instead be found in ``simpeg.maps``. The option of a ``coordinate_system`` for the magnetics simulation is no longer -valid and will throw an ``AttributeError``. Instead use the :class:`SimPEG.maps.SphericalSystem`. +valid and will throw an ``AttributeError``. Instead use the :class:`simpeg.maps.SphericalSystem`. Improvements and Additions to ``resistivity`` --------------------------------------------- -We have made a few improvements to the ``SimPEG.electromagnetics.static.resistivity`` +We have made a few improvements to the ``simpeg.electromagnetics.static.resistivity`` that were motivated by our work under the Geoscientists Without Borders project. One is that we now have a 1D layered Earth simulation class, -:class:`SimPEG.electromagnetics.static.resistivity.simulation_1d.Simulation1DLayers`, +:class:`simpeg.electromagnetics.static.resistivity.simulation_1d.Simulation1DLayers`, that can be used to invert resistivity sounding data for conductivity and/or thicknesses of a set number of layers. diff --git a/docs/content/release/0.14.1-notes.rst b/docs/content/release/0.14.1-notes.rst index da4cb3dc61..a7dd5b1463 100644 --- a/docs/content/release/0.14.1-notes.rst +++ b/docs/content/release/0.14.1-notes.rst @@ -33,7 +33,7 @@ Because of the improvement to the ``resitivity.Fields2D`` object, the previous Resistivity Dipole source and receiver -------------------------------------- -For the ``Dipole`` receiver in ``SimPEG.electromagnetics.static.resistivity.receivers``, +For the ``Dipole`` receiver in ``simpeg.electromagnetics.static.resistivity.receivers``, the ``locationsM`` and ``locationsN`` parameters are deprecated. They are now called ``locations_m`` and ``locations_n``. There are now two ways to create a ``Dipole`` receiver: @@ -47,7 +47,7 @@ or rx = resistivity.receivers.Dipole(locations=(locations_m, locations_n)) -Similarly, for the, ``Dipole`` source in ``SimPEG.electromagnetics.static.resistivity.sources``, +Similarly, for the, ``Dipole`` source in ``simpeg.electromagnetics.static.resistivity.sources``, the ``locationA`` and ``locationB`` parameters are deprecated. They are now called ``location_a`` and ``location_b``. There are now also two ways to create a ``Dipole`` source: diff --git a/docs/content/release/0.14.2-notes.rst b/docs/content/release/0.14.2-notes.rst index 77200797af..ba4dcc8fa8 100644 --- a/docs/content/release/0.14.2-notes.rst +++ b/docs/content/release/0.14.2-notes.rst @@ -13,14 +13,14 @@ New Things ========== @ikding was kind enough to add in more predefined EM waveforms: the -``SimPEG.electromagnetics.time_domain.source.TriangularWaveform`` and -``SimPEG.electromagnetics.time_domain.source.HalfSineWaveform``. +``simpeg.electromagnetics.time_domain.source.TriangularWaveform`` and +``simpeg.electromagnetics.time_domain.source.HalfSineWaveform``. Changes ======= -The defaults solvers located in ``SimPEG.utils.solver_utils`` will now accept arbitrary +The defaults solvers located in ``simpeg.utils.solver_utils`` will now accept arbitrary unused kwargs when wrapping a solver. It will warn the user that a kwarg will be unused. This should allow us to set method specific solver options that wont break the default solvers when they are not used. @@ -32,7 +32,7 @@ Bugs Squashed We unfortunately allowed a small bug in the ``drape_electrodes_to_topography`` function in the last release. This should now work properly for 3D ``discretize.TensorMesh`` again. -There was also a syntax error in ``SimPEG.electromagnetics.frequency_domain.sources.RawVec_m`` +There was also a syntax error in ``simpeg.electromagnetics.frequency_domain.sources.RawVec_m`` that was identified and fixed. There were also typos fixed, type checks were converted to ``isinstance`` checks instead, diff --git a/docs/content/release/0.14.3-notes.rst b/docs/content/release/0.14.3-notes.rst index d095731eae..7a0f34efee 100644 --- a/docs/content/release/0.14.3-notes.rst +++ b/docs/content/release/0.14.3-notes.rst @@ -14,12 +14,12 @@ New Things ========== There is now a file reader for UBC formatted gravity gradient data files. -The class ``SimPEG.electromagnetics.frequency_domain.sources.LineCurrent`` now accepts +The class ``simpeg.electromagnetics.frequency_domain.sources.LineCurrent`` now accepts a keyword argument option ``current`` to set the strength of the ``LineCurrent``. -This is similar behavoir to ```SimPEG.electromagnetics.frequency_domain.sources.CircularLoop``. +This is similar behavoir to ```simpeg.electromagnetics.frequency_domain.sources.CircularLoop``. There are now a few more default options that are setting for the plotting routines -in ``SimPEG.utils.plot_utils.plot2Ddata``. We also now specifically overriding these default +in ``simpeg.utils.plot_utils.plot2Ddata``. We also now specifically overriding these default options for ``norm``, ``levels``, and ``zorder`` for the ``contourOpts`` dictionary input. There is also now a ``streamplotOpts`` dictionary input as well to handle keyword arguments to the stream plot. @@ -35,10 +35,10 @@ SimPEG's coordinate system convention. SimPEG uses a right handed coordinate sys with z being positive upwards, whereas in the UBC file, gravity was defined as positive downwards. -The user provided mapping for ``SimPEG.simulation.LinearSimulation`` was not being +The user provided mapping for ``simpeg.simulation.LinearSimulation`` was not being properly taken into account, and has now been fixed. -We have re-anabled the mu inverse testing of ``SimPEG.electromagnetics.frequency_domain`` +We have re-anabled the mu inverse testing of ``simpeg.electromagnetics.frequency_domain`` classes, ensuring those tests also pass. diff --git a/docs/content/release/0.15.0-notes.rst b/docs/content/release/0.15.0-notes.rst index 0fdc3aab50..8588c79dcb 100644 --- a/docs/content/release/0.15.0-notes.rst +++ b/docs/content/release/0.15.0-notes.rst @@ -13,7 +13,7 @@ Highlights ========== * PGI formulation by @thast * Refactoring of ``static_utils.py`` -* :func:`SimPEG.electromagnetics.frequency_domain.sources.LineCurrent` source +* :func:`simpeg.electromagnetics.frequency_domain.sources.LineCurrent` source * Updates for DC Boundary Conditions * Bug fixes! @@ -44,7 +44,7 @@ Boundary Conditions =================== With the most recent ``0.7.0`` version of ``discretize``, we have updated the DC boundary conditions to flexibly support more mesh types. As part of this, we have also changed -the default boundary condition for the simulations in ``SimPEG.electromagnetics.static.resistivity`` +the default boundary condition for the simulations in ``simpeg.electromagnetics.static.resistivity`` to be the ``Robin`` type condition (equivalent to the ``Mixed`` option previously). This enables both Pole-Pole solutions for the nodal formulation (something that was not possible previously). We also caught a error in the 2D DC resistivity simulations' @@ -52,7 +52,7 @@ previous ``Mixed`` boundary condition option that caused incorrect results. Static Utils ============ -The ``static_utils`` module within ``SimPEG.electromagnetics.static.resistivity`` has +The ``static_utils`` module within ``simpeg.electromagnetics.static.resistivity`` has been given a pass through with many internal changes to make it more flexible in its handling of dc resitivity surveys. Wenner-type arrays should be better supported by the psuedosection plotting utility. It now also includes a 3D psuedosection plotting diff --git a/docs/content/release/0.18.0-notes.rst b/docs/content/release/0.18.0-notes.rst index 751a0d149b..e0c7313a20 100644 --- a/docs/content/release/0.18.0-notes.rst +++ b/docs/content/release/0.18.0-notes.rst @@ -46,7 +46,7 @@ We have deprecated the `Tikhonov` and `Simple` regularizers as it was confusing average user to choose one or the other. They were actually doing very much the same thing, unintentionally, just with slightly different ways of setting parameters for the weighting of the different regularization components. These have now been changed into -a single ``SimPEG.regularization.WeightedLeastSquares`` class. +a single ``simpeg.regularization.WeightedLeastSquares`` class. As a part of this, we have also overhauled the internal workings of regularizations to give a more intuitive user experience to setting different weights for the model diff --git a/docs/content/release/0.19.0-notes.rst b/docs/content/release/0.19.0-notes.rst index db2a6c4fa2..765ad0c709 100644 --- a/docs/content/release/0.19.0-notes.rst +++ b/docs/content/release/0.19.0-notes.rst @@ -31,7 +31,7 @@ sweet on the continuous integration service. ``MetaSimulation`` ------------------ -`SimPEG` contains a new simulation class called ``SimPEG.meta.MetaSimulation``. This experimental +`SimPEG` contains a new simulation class called ``simpeg.meta.MetaSimulation``. This experimental simulation essentially wraps together many simulations into a single simulation. Several common problems can fit into this simulation of simulations model, including tiled (domain decomposition) simulations, time-lapse simulations, or laterally constrained simulations. This approach also is @@ -58,14 +58,14 @@ of a pull request that you should follow. Directive updates ----------------- There are a few new options for determining an initial trade-off parameter for the -regularization functions in inversions. You can now use ``SimPEG.directives.BetaEstimateMaxDerivative`` +regularization functions in inversions. You can now use ``simpeg.directives.BetaEstimateMaxDerivative`` -There are also a few more nobs to turn on ``SimPEG.directives.UpdateSensitivityWeights``, +There are also a few more nobs to turn on ``simpeg.directives.UpdateSensitivityWeights``, controlling how the weights are thresholded and normalized. JointTotalVariation extension ----------------------------- -``SimPEG.regularization.JointTotalVariation`` now supports an arbitrary number of physical +``simpeg.regularization.JointTotalVariation`` now supports an arbitrary number of physical properties models (instead of just two). Many bug fixes diff --git a/docs/content/release/0.21.0-notes.rst b/docs/content/release/0.21.0-notes.rst index 5e668d9dee..f1fd561ba0 100644 --- a/docs/content/release/0.21.0-notes.rst +++ b/docs/content/release/0.21.0-notes.rst @@ -19,7 +19,7 @@ Gravity simulation using Choclo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now we can use a faster and more memory efficient implementation of the gravity -simulation ``SimPEG.potential_fields.gravity.Simulation3DIntegral``, making use +simulation ``simpeg.potential_fields.gravity.Simulation3DIntegral``, making use of Choclo and Numba. To make use of this functionality you will need to `install Choclo `__ in addition to ``SimPEG``. @@ -29,15 +29,15 @@ See https://github.com/simpeg/simpeg/pull/1285. Use Dask with MetaSimulation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A new ``SimPEG.meta.DaskMetaSimulation`` class has been added that allows to -use Dask with ``SimPEG.meta.MetaSimulations``. +A new ``simpeg.meta.DaskMetaSimulation`` class has been added that allows to +use Dask with ``simpeg.meta.MetaSimulations``. See https://github.com/simpeg/simpeg/pull/1199. Rotated Gradients ~~~~~~~~~~~~~~~~~ -Added a new ``SimPEG.regularization.SmoothnessFullGradient`` regularization +Added a new ``simpeg.regularization.SmoothnessFullGradient`` regularization class that allows to regularize first order smoothness along any arbitrary direction, enabling anisotropic weighting. This regularization also works for a ``SimplexMesh``. @@ -47,7 +47,7 @@ See https://github.com/simpeg/simpeg/pull/1167. Logistic Sigmoid Map ~~~~~~~~~~~~~~~~~~~~ -New ``SimPEG.map.LogisticSigmoidMap`` mapping class that computes the logistic +New ``simpeg.map.LogisticSigmoidMap`` mapping class that computes the logistic sigmoid of the model parameters. This is an alternative method to incorporate upper and lower bounds on model parameters. diff --git a/docs/make.bat b/docs/make.bat index 2ac3df69ca..012450a5a7 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -99,9 +99,9 @@ if "%1" == "qthelp" ( echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SimPEG.qhcp + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\simpeg.qhcp echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SimPEG.ghc + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\simpeg.ghc goto end ) diff --git a/examples/01-maps/plot_block_in_layer.py b/examples/01-maps/plot_block_in_layer.py index c84d27b218..66d9cf741d 100644 --- a/examples/01-maps/plot_block_in_layer.py +++ b/examples/01-maps/plot_block_in_layer.py @@ -23,7 +23,7 @@ """ import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_combo.py b/examples/01-maps/plot_combo.py index a7157a4b82..9ba327b66c 100644 --- a/examples/01-maps/plot_combo.py +++ b/examples/01-maps/plot_combo.py @@ -7,9 +7,9 @@ modeling. We will also assume that we are working in log conductivity still, so after the transformation we map to conductivity space. To do this we will introduce the vertical 1D map -(:class:`SimPEG.maps.SurjectVertical1D`), which does the first part of +(:class:`simpeg.maps.SurjectVertical1D`), which does the first part of what we just described. The second part will be done by the -:class:`SimPEG.maps.ExpMap` described above. +:class:`simpeg.maps.ExpMap` described above. .. code-block:: python :linenos: @@ -28,7 +28,7 @@ """ import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_layer.py b/examples/01-maps/plot_layer.py index d73a9bc8bf..6472ae4169 100644 --- a/examples/01-maps/plot_layer.py +++ b/examples/01-maps/plot_layer.py @@ -19,7 +19,7 @@ """ import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index b2063e71bb..b5621e5ae2 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -6,7 +6,7 @@ """ import discretize -from SimPEG import maps, utils +from simpeg import maps, utils import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index aceb6c9220..7cbc4f89c5 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -15,7 +15,7 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -25,7 +25,7 @@ directives, inversion, ) -from SimPEG.potential_fields import magnetics +from simpeg.potential_fields import magnetics import numpy as np import matplotlib.pyplot as plt diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index 37ae5e203d..f5676a5938 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -10,8 +10,8 @@ import numpy as np import matplotlib.pyplot as plt -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -23,7 +23,7 @@ ) from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz -from SimPEG import utils +from simpeg import utils ############################################################################### # Setup diff --git a/examples/03-magnetics/plot_0_analytic.py b/examples/03-magnetics/plot_0_analytic.py index 1c8e7980aa..114aa5c00a 100644 --- a/examples/03-magnetics/plot_0_analytic.py +++ b/examples/03-magnetics/plot_0_analytic.py @@ -7,7 +7,7 @@ """ import numpy as np -from SimPEG.potential_fields.magnetics import analytics +from simpeg.potential_fields.magnetics import analytics import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import make_axes_locatable diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 7cc54915f2..c9e1ab7b4d 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -11,12 +11,12 @@ Cartesian coordinate system, and second for a compact model using the Spherical formulation. -The inverse problem uses the :class:'SimPEG.regularization.Sparse' +The inverse problem uses the :class:'simpeg.regularization.Sparse' that """ -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -27,11 +27,11 @@ regularization, ) -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import active_from_xyz, mesh_builder_xyz, refine_tree_xyz -from SimPEG.potential_fields import magnetics +from simpeg.potential_fields import magnetics import scipy as sp import numpy as np import matplotlib.pyplot as plt diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 0e8740197d..edd69836d2 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -5,12 +5,12 @@ In this example, we demonstrate the use of a Magnetic Vector Inverison on 3D TreeMesh for the inversion of magnetic data. -The inverse problem uses the :class:'SimPEG.regularization.VectorAmplitude' +The inverse problem uses the :class:'simpeg.regularization.VectorAmplitude' regularization borrowed from ... """ -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -21,11 +21,11 @@ regularization, ) -from SimPEG import utils -from SimPEG.utils import mkvc, sdiag +from simpeg import utils +from simpeg.utils import mkvc, sdiag from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz -from SimPEG.potential_fields import magnetics +from simpeg.potential_fields import magnetics import numpy as np import matplotlib.pyplot as plt diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index a7bf711161..d114a30609 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -11,8 +11,8 @@ recover 3-component magnetic data. This data is then transformed to amplitude Secondly, we invert the non-linear inverse problem with -:class:`SimPEG.directives.UpdateSensitivityWeights`. We also -uses the :class:`SimPEG.regularization.Sparse` to apply sparsity +:class:`simpeg.directives.UpdateSensitivityWeights`. We also +uses the :class:`simpeg.regularization.Sparse` to apply sparsity assumption in order to improve the recovery of a compact prism. """ @@ -20,7 +20,7 @@ import scipy as sp import numpy as np import matplotlib.pyplot as plt -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -31,9 +31,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz # sphinx_gallery_thumbnail_number = 4 @@ -137,7 +137,7 @@ )[0] # Assign magnetization value, inducing field strength will -# be applied in by the :class:`SimPEG.PF.Magnetics` problem +# be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e diff --git a/examples/04-dcip/plot_dc_analytic.py b/examples/04-dcip/plot_dc_analytic.py index da9fca2cb3..b47ba4ed2a 100644 --- a/examples/04-dcip/plot_dc_analytic.py +++ b/examples/04-dcip/plot_dc_analytic.py @@ -7,15 +7,15 @@ """ import discretize -from SimPEG import utils +from simpeg import utils import numpy as np import matplotlib.pyplot as plt -from SimPEG.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static import resistivity as DC try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver cs = 25.0 diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py index 6bde3d8dda..7f1ba7dfd2 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py @@ -17,7 +17,7 @@ """ import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -27,14 +27,14 @@ directives, inversion, ) -from SimPEG.electromagnetics.static import resistivity as DC, utils as DCutils +from simpeg.electromagnetics.static import resistivity as DC, utils as DCutils import numpy as np import matplotlib.pyplot as plt try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(12345) diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py index 1f2eac7f9d..8304fce3d6 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py @@ -16,10 +16,10 @@ User is promoted to try different initial values of the parameterized model. """ -from SimPEG.electromagnetics.static import resistivity as DC, utils as DCutils +from simpeg.electromagnetics.static import resistivity as DC, utils as DCutils from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -37,7 +37,7 @@ try: from pymatsolver import PardisoSolver as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run( diff --git a/examples/04-dcip/plot_read_DC_data_with_IO_class.py b/examples/04-dcip/plot_read_DC_data_with_IO_class.py index 28e5707834..0b1d09faa2 100644 --- a/examples/04-dcip/plot_read_DC_data_with_IO_class.py +++ b/examples/04-dcip/plot_read_DC_data_with_IO_class.py @@ -13,9 +13,9 @@ import shutil import os import matplotlib.pyplot as plt -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG import Report -from SimPEG.utils.io_utils import download +from simpeg.electromagnetics.static import resistivity as DC +from simpeg import Report +from simpeg.utils.io_utils import download ############################################################################### # Download an example DC data csv file diff --git a/examples/05-fdem/plot_0_fdem_analytic.py b/examples/05-fdem/plot_0_fdem_analytic.py index 19b2e39a67..24f3ecc75c 100644 --- a/examples/05-fdem/plot_0_fdem_analytic.py +++ b/examples/05-fdem/plot_0_fdem_analytic.py @@ -2,7 +2,7 @@ Simulation with Analytic FDEM Solutions ======================================= -Here, the module *SimPEG.electromagnetics.analytics.FDEM* is used to simulate +Here, the module *simpeg.electromagnetics.analytics.FDEM* is used to simulate harmonic electric and magnetic field for both electric and magnetic dipole sources in a wholespace. @@ -15,8 +15,8 @@ # import numpy as np -from SimPEG import utils -from SimPEG.electromagnetics.analytics.FDEM import ( +from simpeg import utils +from simpeg.electromagnetics.analytics.FDEM import ( ElectricDipoleWholeSpace, MagneticDipoleWholeSpace, ) diff --git a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py index e03c17f1d8..43699d6743 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -7,7 +7,7 @@ We will use only Horizontal co-planar orientations (vertical magnetic dipole), and look at the real and imaginary parts of the secondary magnetic field. -We use the :class:`SimPEG.maps.Surject2Dto3D` mapping to invert for a 2D model +We use the :class:`simpeg.maps.Surject2Dto3D` mapping to invert for a 2D model and perform the forward modelling in 3D. """ @@ -19,10 +19,10 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import discretize -from SimPEG import ( +from simpeg import ( maps, optimization, data_misfit, @@ -32,7 +32,7 @@ directives, Report, ) -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg.electromagnetics import frequency_domain as FDEM ############################################################################### # Setup @@ -271,7 +271,7 @@ def plot_data(data, ax=None, color="C0", label=""): # -------------------- # # We create the data misfit, simple regularization -# (a least-squares-style regularization, :class:`SimPEG.regularization.LeastSquareRegularization`) +# (a least-squares-style regularization, :class:`simpeg.regularization.LeastSquareRegularization`) # The smoothness and smallness contributions can be set by including # `alpha_s, alpha_x, alpha_y` as input arguments when the regularization is # created. The default reference model in the regularization is the starting @@ -281,7 +281,7 @@ def plot_data(data, ax=None, color="C0", label=""): # We estimate the trade-off parameter, beta, between the data # misfit and regularization by the largest eigenvalue of the data misfit and # the regularization. Here, we use a fixed beta, but could alternatively -# employ a beta-cooling schedule using :class:`SimPEG.directives.BetaSchedule` +# employ a beta-cooling schedule using :class:`simpeg.directives.BetaSchedule` dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data) reg = regularization.WeightedLeastSquares(inversion_mesh) diff --git a/examples/06-tdem/plot_0_tdem_analytic.py b/examples/06-tdem/plot_0_tdem_analytic.py index 1a2892b0e0..736d792fb5 100644 --- a/examples/06-tdem/plot_0_tdem_analytic.py +++ b/examples/06-tdem/plot_0_tdem_analytic.py @@ -2,7 +2,7 @@ Simulation with Analytic TDEM Solutions ======================================= -Here, the module *SimPEG.electromagnetics.analytics.TDEM* is used to simulate +Here, the module *simpeg.electromagnetics.analytics.TDEM* is used to simulate transient electric and magnetic field for both electric and magnetic dipole sources in a wholespace. @@ -15,8 +15,8 @@ # import numpy as np -from SimPEG import utils -from SimPEG.electromagnetics.analytics.TDEM import ( +from simpeg import utils +from simpeg.electromagnetics.analytics.TDEM import ( TransientElectricDipoleWholeSpace, TransientMagneticDipoleWholeSpace, ) diff --git a/examples/06-tdem/plot_fwd_tdem_3d_model.py b/examples/06-tdem/plot_fwd_tdem_3d_model.py index 6a07f422ff..def2b65a74 100644 --- a/examples/06-tdem/plot_fwd_tdem_3d_model.py +++ b/examples/06-tdem/plot_fwd_tdem_3d_model.py @@ -9,11 +9,11 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import numpy as np -from SimPEG import maps -from SimPEG.electromagnetics import time_domain as TDEM +from simpeg import maps +from simpeg.electromagnetics import time_domain as TDEM import matplotlib.pyplot as plt ############################################################################### @@ -278,10 +278,10 @@ ############################################################################### -# (E) `SimPEG` +# (E) `simpeg` # ------------ # -# Set-up SimPEG-specific parameters. +# Set-up simpeg-specific parameters. # Set up the receiver list @@ -340,8 +340,8 @@ plt.plot(times, epm_bg * 1e9, ".4", lw=2, label="empymod") -plt.plot(times, spg_bg * 1e9, "C0--", label="SimPEG Background") -plt.plot(times, spg_tg * 1e9, "C1--", label="SimPEG Target") +plt.plot(times, spg_bg * 1e9, "C0--", label="simpeg Background") +plt.plot(times, spg_tg * 1e9, "C1--", label="simpeg Target") plt.ylabel("$E_x$ (nV/m)") plt.xscale("log") @@ -360,4 +360,4 @@ ############################################################################### -# empymod.Report([SimPEG, discretize, pymatsolver]) +# empymod.Report([simpeg, discretize, pymatsolver]) diff --git a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py index 0c7d512844..68f0b668b0 100644 --- a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py +++ b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py @@ -20,11 +20,11 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import time -from SimPEG.electromagnetics import time_domain as TDEM -from SimPEG import utils, maps, Report +from simpeg.electromagnetics import time_domain as TDEM +from simpeg import utils, maps, Report ############################################################################### # Model Parameters diff --git a/examples/06-tdem/plot_fwd_tdem_waveforms.py b/examples/06-tdem/plot_fwd_tdem_waveforms.py index 4ec40ad2f6..c7237e1137 100644 --- a/examples/06-tdem/plot_fwd_tdem_waveforms.py +++ b/examples/06-tdem/plot_fwd_tdem_waveforms.py @@ -8,8 +8,8 @@ import matplotlib.pyplot as plt import numpy as np -from SimPEG.electromagnetics import time_domain as TDEM -from SimPEG.utils import mkvc +from simpeg.electromagnetics import time_domain as TDEM +from simpeg.utils import mkvc nT = 1000 max_t = 5e-3 diff --git a/examples/06-tdem/plot_inv_tdem_1D.py b/examples/06-tdem/plot_inv_tdem_1D.py index b3f6fd1a78..992dbb13fb 100644 --- a/examples/06-tdem/plot_inv_tdem_1D.py +++ b/examples/06-tdem/plot_inv_tdem_1D.py @@ -6,8 +6,8 @@ """ import numpy as np -from SimPEG.electromagnetics import time_domain -from SimPEG import ( +from simpeg.electromagnetics import time_domain +from simpeg import ( optimization, discretize, maps, diff --git a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py index 619ada07e4..06b793bb51 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -9,7 +9,7 @@ import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, data_misfit, regularization, @@ -19,14 +19,14 @@ directives, utils, ) -from SimPEG.electromagnetics import time_domain as TDEM, utils as EMutils +from simpeg.electromagnetics import time_domain as TDEM, utils as EMutils import matplotlib.pyplot as plt from scipy.interpolate import interp1d try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True): diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index 42ddce126a..c5627008e7 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -4,20 +4,20 @@ Forward model 3D MT data. -Test script to use SimPEG.NSEM platform to forward model +Test script to use simpeg.NSEM platform to forward model impedance and tipper synthetic data. """ import discretize -from SimPEG.electromagnetics import natural_source as NSEM -from SimPEG import utils +from simpeg.electromagnetics import natural_source as NSEM +from simpeg import utils import numpy as np import matplotlib.pyplot as plt try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import Solver + from simpeg import Solver def run(plotIt=True): diff --git a/examples/08-vrm/plot_fwd_vrm.py b/examples/08-vrm/plot_fwd_vrm.py index db7e0a27ce..fba26e608f 100644 --- a/examples/08-vrm/plot_fwd_vrm.py +++ b/examples/08-vrm/plot_fwd_vrm.py @@ -5,7 +5,7 @@ Here, we predict the vertical db/dt response over a conductive and magnetically viscous Earth for a small coincident loop system. Following the theory, the total response is approximately equal to the sum of the -inductive and VRM responses modelled separately. The SimPEG.VRM module is +inductive and VRM responses modelled separately. The simpeg.VRM module is used to model the VRM response while an analytic solution for a conductive half-space is used to model the inductive response. """ @@ -15,10 +15,10 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as VRM +from simpeg.electromagnetics import viscous_remanent_magnetization as VRM import numpy as np import discretize -from SimPEG import mkvc, maps +from simpeg import mkvc, maps import matplotlib.pyplot as plt import matplotlib as mpl diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index dab9190535..a8558023c8 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -16,10 +16,10 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as VRM +from simpeg.electromagnetics import viscous_remanent_magnetization as VRM import numpy as np import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, diff --git a/examples/09-flow/plot_fwd_flow_richards_1D.py b/examples/09-flow/plot_fwd_flow_richards_1D.py index dcc2a7b7ff..1ce540fa12 100644 --- a/examples/09-flow/plot_fwd_flow_richards_1D.py +++ b/examples/09-flow/plot_fwd_flow_richards_1D.py @@ -44,8 +44,8 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index d38dbb4014..567e41f112 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -31,15 +31,15 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG import regularization -from SimPEG import data_misfit -from SimPEG import optimization -from SimPEG import inverse_problem -from SimPEG import directives -from SimPEG import inversion - -from SimPEG.flow import richards +from simpeg import maps +from simpeg import regularization +from simpeg import data_misfit +from simpeg import optimization +from simpeg import inverse_problem +from simpeg import directives +from simpeg import inversion + +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py index ca142d9bcb..86ba4c2572 100644 --- a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py +++ b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py @@ -15,7 +15,7 @@ import discretize as Mesh import matplotlib.pyplot as plt import numpy as np -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, @@ -65,7 +65,7 @@ def g(k): mtrue[mesh.cell_centers_x > 0.45] = -0.5 mtrue[mesh.cell_centers_x > 0.6] = 0 -# SimPEG problem and survey +# simpeg problem and survey prob = simulation.LinearSimulation(mesh, G=G, model_map=maps.IdentityMap()) std = 0.01 survey = prob.make_synthetic_data(mtrue, relative_error=std, add_noise=True) diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index afaa07b183..693139ab38 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -12,7 +12,7 @@ import discretize as Mesh import matplotlib.pyplot as plt import numpy as np -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, diff --git a/examples/20-published/plot_booky_1D_time_freq_inv.py b/examples/20-published/plot_booky_1D_time_freq_inv.py index e1469d3f3d..dd42c3938d 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -7,7 +7,7 @@ `https://storage.googleapis.com/simpeg/bookpurnong/bookpurnong.tar.gz `_ The forward simulation is performed on the cylindrically symmetric mesh using -:code:`SimPEG.electromagnetics.frequency_domain`, and :code:`SimPEG.electromagnetics.time_domain` +:code:`simpeg.electromagnetics.frequency_domain`, and :code:`simpeg.electromagnetics.time_domain` The RESOLVE data are inverted first. This recovered model is then used as a reference model for the SkyTEM inversion @@ -36,7 +36,7 @@ from pymatsolver import Pardiso as Solver import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -47,7 +47,7 @@ directives, data, ) -from SimPEG.electromagnetics import frequency_domain as FDEM, time_domain as TDEM +from simpeg.electromagnetics import frequency_domain as FDEM, time_domain as TDEM def download_and_unzip_data( diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index dbdff9966b..ec900f6da8 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -7,7 +7,7 @@ `https://storage.googleapis.com/simpeg/bookpurnong/bookpurnong.tar.gz `_ The forward simulation is performed on the cylindrically symmetric mesh using -:code:`SimPEG.electromagnetics.frequency_domain`. +:code:`simpeg.electromagnetics.frequency_domain`. Lindsey J. Heagy, Rowan Cockett, Seogi Kang, Gudni K. Rosenkjaer, Douglas W. Oldenburg, A framework for simulation and inversion in electromagnetics, @@ -32,7 +32,7 @@ from scipy.spatial import cKDTree import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -43,7 +43,7 @@ directives, data, ) -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg.electromagnetics import frequency_domain as FDEM def download_and_unzip_data( diff --git a/examples/20-published/plot_effective_medium_theory.py b/examples/20-published/plot_effective_medium_theory.py index 75ec5c89e8..f3f2d4b604 100644 --- a/examples/20-published/plot_effective_medium_theory.py +++ b/examples/20-published/plot_effective_medium_theory.py @@ -5,7 +5,7 @@ This example uses Self Consistent Effective Medium Theory to estimate the electrical conductivity of a mixture of two phases of materials. Given the electrical conductivity of each of the phases (:math:`\sigma_0`, -:math:`\sigma_1`), the :class:`SimPEG.maps.SelfConsistentEffectiveMedium` +:math:`\sigma_1`), the :class:`simpeg.maps.SelfConsistentEffectiveMedium` map takes the concentration of phase-1 (:math:`\phi_1`) and maps this to an electrical conductivity. @@ -20,7 +20,7 @@ import numpy as np import matplotlib.pyplot as plt -from SimPEG import maps +from simpeg import maps from matplotlib import rcParams rcParams["font.size"] = 12 diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index bcce56721a..e9f817c04d 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -32,9 +32,9 @@ """ import discretize -from SimPEG import utils, maps, tests -from SimPEG.electromagnetics import frequency_domain as FDEM, mu_0 -from SimPEG.utils.io_utils import download +from simpeg import utils, maps, tests +from simpeg.electromagnetics import frequency_domain as FDEM, mu_0 +from simpeg.utils.io_utils import download # try: # from pymatsolver import MumpsSolver as Solver @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import numpy as np import scipy.sparse as sp diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 2c93524648..53f328aeaf 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -20,7 +20,7 @@ """ import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -31,14 +31,14 @@ directives, ) import numpy as np -from SimPEG.electromagnetics import frequency_domain as FDEM, time_domain as TDEM, mu_0 +from simpeg.electromagnetics import frequency_domain as FDEM, time_domain as TDEM, mu_0 import matplotlib.pyplot as plt import matplotlib try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, saveFig=False): diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index 47a467c343..d4dd2ffca5 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -15,8 +15,8 @@ import os import shutil import tarfile -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( data_misfit, maps, regularization, @@ -25,11 +25,11 @@ directives, inversion, ) -from SimPEG.utils import download, plot2Ddata +from simpeg.utils import download, plot2Ddata import matplotlib.pyplot as plt import numpy as np -from SimPEG.utils.drivers.gravity_driver import GravityDriver_Inv +from simpeg.utils.drivers.gravity_driver import GravityDriver_Inv def run(plotIt=True, cleanAfterRun=True): diff --git a/examples/20-published/plot_load_booky.py b/examples/20-published/plot_load_booky.py index c582c5b245..7708279cf6 100644 --- a/examples/20-published/plot_load_booky.py +++ b/examples/20-published/plot_load_booky.py @@ -25,7 +25,7 @@ import tarfile import os import shutil -from SimPEG import utils +from simpeg import utils import discretize diff --git a/examples/20-published/plot_richards_celia1990.py b/examples/20-published/plot_richards_celia1990.py index ce2267d8b9..2c93764cd2 100644 --- a/examples/20-published/plot_richards_celia1990.py +++ b/examples/20-published/plot_richards_celia1990.py @@ -44,8 +44,8 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index 478d8b90e3..6654e0ad08 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -48,14 +48,14 @@ import matplotlib.pylab as plt import numpy as np import discretize -from SimPEG import maps, utils -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg import maps, utils +from simpeg.electromagnetics import frequency_domain as FDEM import time try: from pymatsolver import Pardiso as Solver except Exception: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True): diff --git a/examples/20-published/plot_tomo_joint_with_volume.py b/examples/20-published/plot_tomo_joint_with_volume.py index 791bc32a8c..1de1d798f4 100644 --- a/examples/20-published/plot_tomo_joint_with_volume.py +++ b/examples/20-published/plot_tomo_joint_with_volume.py @@ -22,9 +22,9 @@ import scipy.sparse as sp import matplotlib.pyplot as plt -from SimPEG.seismic import straight_ray_tomography as tomo +from simpeg.seismic import straight_ray_tomography as tomo import discretize -from SimPEG import ( +from simpeg import ( maps, utils, regularization, diff --git a/examples/20-published/plot_vadose_vangenuchten.py b/examples/20-published/plot_vadose_vangenuchten.py index a05cb0b6f4..03f3ea997b 100644 --- a/examples/20-published/plot_vadose_vangenuchten.py +++ b/examples/20-published/plot_vadose_vangenuchten.py @@ -14,7 +14,7 @@ import matplotlib.pyplot as plt import discretize -from SimPEG.flow import richards +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/_archived/plot_inv_dcip_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_2_5Dinversion.py index ba8ba33d03..717cf0cb5c 100644 --- a/examples/_archived/plot_inv_dcip_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_2_5Dinversion.py @@ -17,10 +17,10 @@ subsequent IP inversion to recover a chargeability model. """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static import induced_polarization as IP -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import maps, utils +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static import induced_polarization as IP +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import maps, utils from discretize.utils import active_from_xyz import matplotlib.pyplot as plt from matplotlib import colors @@ -30,7 +30,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole"): diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index 465390fca9..dafcfee6a9 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -12,9 +12,9 @@ 'dipole-pole', and 'pole-pole'. """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import ( +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import ( maps, utils, data_misfit, @@ -33,7 +33,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole"): diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index 8c3238670b..a2c67c696a 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -20,9 +20,9 @@ """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import ( +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import ( maps, utils, data_misfit, @@ -41,7 +41,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index c35831bd76..7d6e07ab05 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -12,8 +12,8 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -24,8 +24,8 @@ inversion, ) -from SimPEG import utils -from SimPEG.utils import plot2Ddata +from simpeg import utils +from simpeg.utils import plot2Ddata def run(plotIt=True): diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index bf25676a50..8656f1dce1 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -11,9 +11,9 @@ import numpy as np from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG import ( +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg import ( data, data_misfit, maps, diff --git a/setup.py b/setup.py index 9e55ae825a..10411d654e 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,9 @@ LONG_DESCRIPTION = "".join(f.readlines()) setup( - name="SimPEG", + name="simpeg", packages=find_packages(exclude=["tests*", "examples*", "tutorials*"]), + py_modules=["SimPEG"], python_requires=">=3.8", setup_requires=[ "setuptools_scm", @@ -57,6 +58,6 @@ platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], use_2to3=False, use_scm_version={ - "write_to": os.path.join("SimPEG", "version.py"), + "write_to": os.path.join("simpeg", "version.py"), }, ) diff --git a/SimPEG/__init__.py b/simpeg/__init__.py similarity index 96% rename from SimPEG/__init__.py rename to simpeg/__init__.py index d6ef51cf2a..36996feac9 100644 --- a/SimPEG/__init__.py +++ b/simpeg/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Base SimPEG Classes (:mod:`SimPEG`) +Base SimPEG Classes (:mod:`simpeg`) ======================================================== -.. currentmodule:: SimPEG +.. currentmodule:: simpeg SimPEG is built off of several base classes that define the general structure of simulations and inversion operations. @@ -174,7 +174,7 @@ # - Released versions just tags: 0.8.0 # - GitHub commits add .dev#+hash: 0.8.1.dev4+g2785721 # - Uncommitted changes add timestamp: 0.8.1.dev4+g2785721.d20191022 - from SimPEG.version import version as __version__ + from simpeg.version import version as __version__ except ImportError: # If it was not installed, then we don't know the version. We could throw a # warning here, but this case *should* be rare. SimPEG should be diff --git a/SimPEG/base/__init__.py b/simpeg/base/__init__.py similarity index 100% rename from SimPEG/base/__init__.py rename to simpeg/base/__init__.py diff --git a/SimPEG/base/pde_simulation.py b/simpeg/base/pde_simulation.py similarity index 100% rename from SimPEG/base/pde_simulation.py rename to simpeg/base/pde_simulation.py diff --git a/simpeg/dask/__init__.py b/simpeg/dask/__init__.py new file mode 100644 index 0000000000..e26bc036f5 --- /dev/null +++ b/simpeg/dask/__init__.py @@ -0,0 +1,10 @@ +try: + import simpeg.dask.simulation + import simpeg.dask.electromagnetics.static.resistivity.simulation + import simpeg.dask.electromagnetics.static.induced_polarization.simulation + import simpeg.dask.potential_fields.base + import simpeg.dask.potential_fields.gravity.simulation + import simpeg.dask.potential_fields.magnetics.simulation +except ImportError as err: + print("unable to load dask operations") + print(err) diff --git a/SimPEG/dask/electromagnetics/__init__.py b/simpeg/dask/electromagnetics/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/__init__.py rename to simpeg/dask/electromagnetics/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/__init__.py b/simpeg/dask/electromagnetics/static/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/__init__.py rename to simpeg/dask/electromagnetics/static/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/__init__.py b/simpeg/dask/electromagnetics/static/induced_polarization/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/induced_polarization/__init__.py rename to simpeg/dask/electromagnetics/static/induced_polarization/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py rename to simpeg/dask/electromagnetics/static/induced_polarization/simulation.py diff --git a/SimPEG/dask/electromagnetics/static/resistivity/__init__.py b/simpeg/dask/electromagnetics/static/resistivity/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/resistivity/__init__.py rename to simpeg/dask/electromagnetics/static/resistivity/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/resistivity/simulation.py b/simpeg/dask/electromagnetics/static/resistivity/simulation.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/resistivity/simulation.py rename to simpeg/dask/electromagnetics/static/resistivity/simulation.py diff --git a/SimPEG/dask/potential_fields/__init__.py b/simpeg/dask/potential_fields/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/__init__.py rename to simpeg/dask/potential_fields/__init__.py diff --git a/SimPEG/dask/potential_fields/base.py b/simpeg/dask/potential_fields/base.py similarity index 100% rename from SimPEG/dask/potential_fields/base.py rename to simpeg/dask/potential_fields/base.py diff --git a/SimPEG/dask/potential_fields/gravity/__init__.py b/simpeg/dask/potential_fields/gravity/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/gravity/__init__.py rename to simpeg/dask/potential_fields/gravity/__init__.py diff --git a/SimPEG/dask/potential_fields/gravity/simulation.py b/simpeg/dask/potential_fields/gravity/simulation.py similarity index 100% rename from SimPEG/dask/potential_fields/gravity/simulation.py rename to simpeg/dask/potential_fields/gravity/simulation.py diff --git a/SimPEG/dask/potential_fields/magnetics/__init__.py b/simpeg/dask/potential_fields/magnetics/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/magnetics/__init__.py rename to simpeg/dask/potential_fields/magnetics/__init__.py diff --git a/SimPEG/dask/potential_fields/magnetics/simulation.py b/simpeg/dask/potential_fields/magnetics/simulation.py similarity index 100% rename from SimPEG/dask/potential_fields/magnetics/simulation.py rename to simpeg/dask/potential_fields/magnetics/simulation.py diff --git a/SimPEG/dask/simulation.py b/simpeg/dask/simulation.py similarity index 100% rename from SimPEG/dask/simulation.py rename to simpeg/dask/simulation.py diff --git a/SimPEG/dask/utils.py b/simpeg/dask/utils.py similarity index 100% rename from SimPEG/dask/utils.py rename to simpeg/dask/utils.py diff --git a/SimPEG/data.py b/simpeg/data.py similarity index 98% rename from SimPEG/data.py rename to simpeg/data.py index fa42a6d59e..1d53972caa 100644 --- a/SimPEG/data.py +++ b/simpeg/data.py @@ -15,7 +15,7 @@ class Data: Parameters ---------- - survey : SimPEG.survey.BaseSurvey + survey : simpeg.survey.BaseSurvey A SimPEG survey object. For each geophysical method, the survey object defines the survey geometry; i.e. sources, receivers, data type. dobs : (n) numpy.ndarray @@ -98,7 +98,7 @@ def survey(self): Returns ------- - SimPEG.simulation.BaseSurvey + simpeg.simulation.BaseSurvey """ return self._survey @@ -365,12 +365,12 @@ def fromvec(self, v): class SyntheticData(Data): r"""Synthetic data class. - The ``SyntheticData`` class is a :py:class:`SimPEG.data.Data` class that allows the + The ``SyntheticData`` class is a :py:class:`simpeg.data.Data` class that allows the user to keep track of both clean and noisy data. Parameters ---------- - survey : SimPEG.survey.BaseSurvey + survey : simpeg.survey.BaseSurvey A SimPEG survey object. For each geophysical method, the survey object defines the survey geometry; i.e. sources, receivers, data type. dobs : numpy.ndarray diff --git a/SimPEG/data_misfit.py b/simpeg/data_misfit.py similarity index 95% rename from SimPEG/data_misfit.py rename to simpeg/data_misfit.py index 6d975f2d30..6b489b425a 100644 --- a/SimPEG/data_misfit.py +++ b/simpeg/data_misfit.py @@ -11,7 +11,7 @@ class BaseDataMisfit(L2ObjectiveFunction): r"""Base data misfit class. Inherit this class to build your own data misfit function. The ``BaseDataMisfit`` - class inherits the :py:class:`SimPEG.objective_function.L2ObjectiveFunction`. + class inherits the :py:class:`simpeg.objective_function.L2ObjectiveFunction`. And as a result, it is limited to building data misfit functions of the form: .. important:: @@ -26,13 +26,13 @@ class inherits the :py:class:`SimPEG.objective_function.L2ObjectiveFunction`. Parameters ---------- - data : SimPEG.data.Data + data : simpeg.data.Data A SimPEG data object. - simulation : SimPEG.simulation.BaseSimulation + simulation : simpeg.simulation.BaseSimulation A SimPEG simulation object. debug : bool Print debugging information. - counter : None or SimPEG.utils.Counter + counter : None or simpeg.utils.Counter Assign a SimPEG ``Counter`` object to store iterations and run-times. """ @@ -48,7 +48,7 @@ def data(self): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. """ return self._data @@ -63,7 +63,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseSimulation + simpeg.simulation.BaseSimulation A SimPEG simulation object. """ return self._simulation @@ -95,7 +95,7 @@ def counter(self): Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter SimPEG ``Counter`` object to store iterations and run-times. """ return self._counter @@ -216,7 +216,7 @@ def residual(self, m, f=None): ---------- m : (n_param, ) numpy.ndarray The model for which the function is evaluated. - f : None or SimPEG.fields.Fields, optional + f : None or simpeg.fields.Fields, optional A SimPEG fields object. Used when the fields for the model *m* have already been computed. @@ -251,13 +251,13 @@ class L2DataMisfit(BaseDataMisfit): Parameters ---------- - data : SimPEG.data.Data + data : simpeg.data.Data A SimPEG data object that has observed data and uncertainties. - simulation : SimPEG.simulation.BaseSimulation + simulation : simpeg.simulation.BaseSimulation A SimPEG simulation object. debug : bool Print debugging information. - counter : None or SimPEG.utils.Counter + counter : None or simpeg.utils.Counter Assign a SimPEG ``Counter`` object to store iterations and run-times. """ diff --git a/SimPEG/directives/__init__.py b/simpeg/directives/__init__.py similarity index 94% rename from SimPEG/directives/__init__.py rename to simpeg/directives/__init__.py index 8ce839d89f..69081767d1 100644 --- a/SimPEG/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -1,13 +1,13 @@ """ ============================================= -Directives (:mod:`SimPEG.directives`) +Directives (:mod:`simpeg.directives`) ============================================= -.. currentmodule:: SimPEG.directives +.. currentmodule:: simpeg.directives Directives are classes that allow us to control the inversion, perform tasks between iterations, save information about our inversion process and more. -Directives are passed to the ``SimPEG.inversion.BaseInversion`` class through +Directives are passed to the ``simpeg.inversion.BaseInversion`` class through the ``directiveList`` argument. The tasks specified through the directives are executed after each inversion iteration, following the same order as in which they are passed in the ``directiveList``. @@ -87,7 +87,7 @@ The ``InversionDirective`` class defines the basic class for all directives. Inherit from this class when writing your own directive. The ``DirectiveList`` is used under the hood to handle the execution of all directives passed to the -``SimPEG.inversion.BaseInversion``. +``simpeg.inversion.BaseInversion``. .. autosummary:: :toctree: generated/ diff --git a/SimPEG/directives/directives.py b/simpeg/directives/directives.py similarity index 98% rename from SimPEG/directives/directives.py rename to simpeg/directives/directives.py index 3907ca646e..21659338f1 100644 --- a/SimPEG/directives/directives.py +++ b/simpeg/directives/directives.py @@ -48,12 +48,12 @@ class InversionDirective: Parameters ---------- - inversion : SimPEG.inversion.BaseInversion, None - An SimPEG inversion object; i.e. an instance of :class:`SimPEG.inversion.BaseInversion`. - dmisfit : SimPEG.data_misfit.BaseDataMisfit, None - A data data misfit; i.e. an instance of :class:`SimPEG.data_misfit.BaseDataMisfit`. - reg : SimPEG.regularization.BaseRegularization, None - The regularization, or model objective function; i.e. an instance of :class:`SimPEG.regularization.BaseRegularization`. + inversion : simpeg.inversion.BaseInversion, None + An SimPEG inversion object; i.e. an instance of :class:`simpeg.inversion.BaseInversion`. + dmisfit : simpeg.data_misfit.BaseDataMisfit, None + A data data misfit; i.e. an instance of :class:`simpeg.data_misfit.BaseDataMisfit`. + reg : simpeg.regularization.BaseRegularization, None + The regularization, or model objective function; i.e. an instance of :class:`simpeg.regularization.BaseRegularization`. verbose : bool Whether or not to print debugging information. """ @@ -195,7 +195,7 @@ def survey(self): Returns ------- - list of SimPEG.survey.Survey + list of simpeg.survey.Survey Survey for all data misfits. """ return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] @@ -210,7 +210,7 @@ def simulation(self): Returns ------- - list of SimPEG.simulation.BaseSimulation + list of simpeg.simulation.BaseSimulation Simulation for all data misfits. """ return [objfcts.simulation for objfcts in self.dmisfit.objfcts] @@ -412,7 +412,7 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): The initial trade-off parameter (beta) is estimated by scaling the ratio between the largest derivatives in the gradient of the data misfit and model objective function. The estimated trade-off parameter is used to - update the **beta** property in the associated :class:`SimPEG.inverse_problem.BaseInvProblem` + update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. A separate directive is used for updating the trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. @@ -479,9 +479,9 @@ class BetaEstimate_ByEig(BaseBetaEstimator): The initial trade-off parameter (beta) is estimated by scaling the ratio between the largest eigenvalue in the second derivative of the data misfit and the model objective function. The largest eigenvalues are estimated - using the power iteration method; see :func:`SimPEG.utils.eigenvalue_by_power_iteration`. + using the power iteration method; see :func:`simpeg.utils.eigenvalue_by_power_iteration`. The estimated trade-off parameter is used to update the **beta** property in the - associated :class:`SimPEG.inverse_problem.BaseInvProblem` object prior to running the inversion. + associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. Note that a separate directive is used for updating the trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. @@ -517,7 +517,7 @@ class BetaEstimate_ByEig(BaseBetaEstimator): parameter 'n_pw_iter' sets the number of power iterations used in the estimate. For a description of the power iteration approach for estimating the larges eigenvalue, - see :func:`SimPEG.utils.eigenvalue_by_power_iteration`. + see :func:`simpeg.utils.eigenvalue_by_power_iteration`. """ @@ -568,7 +568,7 @@ def initialize(self): class BetaSchedule(InversionDirective): """Reduce trade-off parameter (beta) at successive iterations using a cooling schedule. - Updates the **beta** property in the associated :class:`SimPEG.inverse_problem.BaseInvProblem` + Updates the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` while the inversion is running. For linear least-squares problems, the optimization problem can be solved in a single step and the cooling rate can be set to *1*. For non-linear optimization @@ -1614,7 +1614,7 @@ class SaveModelEveryIteration(SaveEveryIteration): def initialize(self): print( - "SimPEG.SaveModelEveryIteration will save your models as: " + "simpeg.SaveModelEveryIteration will save your models as: " "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) ) @@ -1652,7 +1652,7 @@ def save_txt(self, value): def initialize(self): if self.save_txt is True: print( - "SimPEG.SaveOutputEveryIteration will save your inversion " + "simpeg.SaveOutputEveryIteration will save your inversion " "progress as: '###-{0!s}.txt'".format(self.fileName) ) f = open(self.fileName + ".txt", "w") @@ -1875,7 +1875,7 @@ def initialize(self): self.outDict = {} if self.saveOnDisk: print( - "SimPEG.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( + "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( self.fileName ) ) @@ -2434,7 +2434,7 @@ class UpdateSensitivityWeights(InversionDirective): The underlying theory is provided below in the `Notes` section. This directive **requires** that the map for the regularization function is either - class:`SimPEG.maps.Wires` or class:`SimPEG.maps.Identity`. In other words, the + class:`simpeg.maps.Wires` or class:`simpeg.maps.Identity`. In other words, the sensitivity weighting cannot be applied for parametric inversion. In addition, the simulation(s) connected to the inverse problem **must** have a ``getJ`` or ``getJtJdiag`` method. diff --git a/SimPEG/directives/pgi_directives.py b/simpeg/directives/pgi_directives.py similarity index 98% rename from SimPEG/directives/pgi_directives.py rename to simpeg/directives/pgi_directives.py index 0cc141f026..60f4488b90 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/simpeg/directives/pgi_directives.py @@ -270,12 +270,12 @@ def update_previous_dmlist(self): @property def directives(self): - """List of all the directives in the :class:`SimPEG.inverison.BaseInversion``.""" + """List of all the directives in the :class:`simpeg.inverison.BaseInversion``.""" return self.inversion.directiveList.dList @property def multi_target_misfits_directive(self): - """``MultiTargetMisfit`` directive in the :class:`SimPEG.inverison.BaseInversion``.""" + """``MultiTargetMisfit`` directive in the :class:`simpeg.inverison.BaseInversion``.""" if not hasattr(self, "_mtm_directive"): # Obtain multi target misfits directive from the directive list multi_target_misfits_directive = [ @@ -294,7 +294,7 @@ def multi_target_misfits_directive(self): @property def pgi_update_params_directive(self): - """``PGI_UpdateParam``s directive in the :class:`SimPEG.inverison.BaseInversion``.""" + """``PGI_UpdateParam``s directive in the :class:`simpeg.inverison.BaseInversion``.""" if not hasattr(self, "_pgi_update_params"): # Obtain PGI_UpdateParams directive from the directive list pgi_update_params_directive = [ @@ -310,7 +310,7 @@ def pgi_update_params_directive(self): @property def pgi_regularization(self): - """PGI regularization in the :class:`SimPEG.inverse_problem.BaseInvProblem``.""" + """PGI regularization in the :class:`simpeg.inverse_problem.BaseInvProblem``.""" if not hasattr(self, "_pgi_regularization"): pgi_regularization = self.reg.get_functions_of_type(PGI) if len(pgi_regularization) != 1: diff --git a/SimPEG/directives/sim_directives.py b/simpeg/directives/sim_directives.py similarity index 100% rename from SimPEG/directives/sim_directives.py rename to simpeg/directives/sim_directives.py diff --git a/SimPEG/electromagnetics/__init__.py b/simpeg/electromagnetics/__init__.py similarity index 92% rename from SimPEG/electromagnetics/__init__.py rename to simpeg/electromagnetics/__init__.py index 5deacd0f50..23a0b24d9e 100644 --- a/SimPEG/electromagnetics/__init__.py +++ b/simpeg/electromagnetics/__init__.py @@ -1,8 +1,8 @@ """ ================================================================= -Base EM (:mod:`SimPEG.electromagnetics`) +Base EM (:mod:`simpeg.electromagnetics`) ================================================================= -.. currentmodule:: SimPEG.electromagnetics +.. currentmodule:: simpeg.electromagnetics About ``electromagnetics`` diff --git a/SimPEG/electromagnetics/analytics/DC.py b/simpeg/electromagnetics/analytics/DC.py similarity index 100% rename from SimPEG/electromagnetics/analytics/DC.py rename to simpeg/electromagnetics/analytics/DC.py diff --git a/SimPEG/electromagnetics/analytics/FDEM.py b/simpeg/electromagnetics/analytics/FDEM.py similarity index 98% rename from SimPEG/electromagnetics/analytics/FDEM.py rename to simpeg/electromagnetics/analytics/FDEM.py index b34fa85d00..c36027f458 100644 --- a/SimPEG/electromagnetics/analytics/FDEM.py +++ b/simpeg/electromagnetics/analytics/FDEM.py @@ -1,6 +1,6 @@ import numpy as np from scipy.constants import mu_0, pi, epsilon_0 -from SimPEG import utils +from simpeg import utils def hzAnalyticDipoleF(r, freq, sigma, secondary=True, mu=mu_0): @@ -12,7 +12,7 @@ def hzAnalyticDipoleF(r, freq, sigma, secondary=True, mu=mu_0): Examples -------- >>> import matplotlib.pyplot as plt - >>> from SimPEG import electromagnetics as em + >>> from simpeg import electromagnetics as em >>> freq = np.logspace(-1, 5, 301) >>> test = em.analytics.hzAnalyticDipoleF( >>> 100, freq, 0.01, secondary=False) @@ -75,7 +75,7 @@ def MagneticDipoleWholeSpace( .. plot:: import numpy as np - from SimPEG import electromagnetics as EM + from simpeg import electromagnetics as EM import matplotlib.pyplot as plt from scipy.constants import mu_0 freqs = np.logspace(-2, 5, 301) diff --git a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py similarity index 99% rename from SimPEG/electromagnetics/analytics/FDEMDipolarfields.py rename to simpeg/electromagnetics/analytics/FDEMDipolarfields.py index 9eebd4fd0f..bfdfee7748 100644 --- a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py +++ b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py @@ -1,6 +1,6 @@ import numpy as np from scipy.constants import epsilon_0, mu_0 -from SimPEG import utils +from simpeg import utils def omega(f): diff --git a/SimPEG/electromagnetics/analytics/FDEMcasing.py b/simpeg/electromagnetics/analytics/FDEMcasing.py similarity index 99% rename from SimPEG/electromagnetics/analytics/FDEMcasing.py rename to simpeg/electromagnetics/analytics/FDEMcasing.py index 6088aaf9ef..0196d1277b 100644 --- a/SimPEG/electromagnetics/analytics/FDEMcasing.py +++ b/simpeg/electromagnetics/analytics/FDEMcasing.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0, epsilon_0 -from SimPEG.electromagnetics.utils import k +from simpeg.electromagnetics.utils import k def getKc(freq, sigma, a, b, mu=mu_0, eps=epsilon_0): diff --git a/SimPEG/electromagnetics/analytics/NSEM.py b/simpeg/electromagnetics/analytics/NSEM.py similarity index 99% rename from SimPEG/electromagnetics/analytics/NSEM.py rename to simpeg/electromagnetics/analytics/NSEM.py index 3df2612320..33a6772d4a 100644 --- a/SimPEG/electromagnetics/analytics/NSEM.py +++ b/simpeg/electromagnetics/analytics/NSEM.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import epsilon_0 from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils import k, omega +from simpeg.electromagnetics.utils import k, omega __all__ = ["MT_LayeredEarth"] diff --git a/SimPEG/electromagnetics/analytics/TDEM.py b/simpeg/electromagnetics/analytics/TDEM.py similarity index 99% rename from SimPEG/electromagnetics/analytics/TDEM.py rename to simpeg/electromagnetics/analytics/TDEM.py index b5b1ee9a04..c73ca0a815 100644 --- a/SimPEG/electromagnetics/analytics/TDEM.py +++ b/simpeg/electromagnetics/analytics/TDEM.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0, pi from scipy.special import erf -from SimPEG import utils +from simpeg import utils def hzAnalyticDipoleT(r, t, sigma): diff --git a/SimPEG/electromagnetics/analytics/__init__.py b/simpeg/electromagnetics/analytics/__init__.py similarity index 100% rename from SimPEG/electromagnetics/analytics/__init__.py rename to simpeg/electromagnetics/analytics/__init__.py diff --git a/SimPEG/electromagnetics/base.py b/simpeg/electromagnetics/base.py similarity index 97% rename from SimPEG/electromagnetics/base.py rename to simpeg/electromagnetics/base.py index 2a2d9c5ebd..b57836d5b4 100644 --- a/SimPEG/electromagnetics/base.py +++ b/simpeg/electromagnetics/base.py @@ -57,7 +57,7 @@ class BaseEMSrc(BaseSrc): ---------- location : (n_dim) numpy.ndarray Location of the source - receiver_list : list of SimPEG.survey.BaseRx objects + receiver_list : list of simpeg.survey.BaseRx objects Sets the receivers associated with the source uid : uuid.UUID A universally unique identifier @@ -93,7 +93,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An instance of an electromagnetic simulation Returns @@ -111,7 +111,7 @@ def evalDeriv(self, simulation, v=None, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An instance of an electromagnetic simulation v : np.ndarray A vector diff --git a/SimPEG/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py similarity index 100% rename from SimPEG/electromagnetics/base_1d.py rename to simpeg/electromagnetics/base_1d.py diff --git a/SimPEG/electromagnetics/frequency_domain/__init__.py b/simpeg/electromagnetics/frequency_domain/__init__.py similarity index 93% rename from SimPEG/electromagnetics/frequency_domain/__init__.py rename to simpeg/electromagnetics/frequency_domain/__init__.py index 33ed705f2a..5ec6ac0a13 100644 --- a/SimPEG/electromagnetics/frequency_domain/__init__.py +++ b/simpeg/electromagnetics/frequency_domain/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================== -Frequency-Domain EM (:mod:`SimPEG.electromagnetics.frequency_domain`) +Frequency-Domain EM (:mod:`simpeg.electromagnetics.frequency_domain`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.frequency_domain +.. currentmodule:: simpeg.electromagnetics.frequency_domain About ``frequency_domain`` diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py similarity index 98% rename from SimPEG/electromagnetics/frequency_domain/fields.py rename to simpeg/electromagnetics/frequency_domain/fields.py index 06900f5dc3..ed950b91f7 100644 --- a/SimPEG/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -153,7 +153,7 @@ def _eDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{e}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -185,7 +185,7 @@ def _bDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{b}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -217,7 +217,7 @@ def _bSecondaryDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{b}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: sorce + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: sorce :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -236,7 +236,7 @@ def _hDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{h}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -268,7 +268,7 @@ def _jDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{j}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -299,7 +299,7 @@ class Fields3DElectricField(FieldsFDEM): Fields object for Simulation3DElectricField. :param discretize.base.BaseMesh mesh: mesh - :param SimPEG.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ knownFields = {"eSolution": "E"} @@ -395,7 +395,7 @@ def _eDeriv_m(self, src, v, adjoint=False): :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the electric field derivative with respect to the inversion model with a vector """ @@ -627,7 +627,7 @@ class Fields3DMagneticFluxDensity(FieldsFDEM): Fields object for Simulation3DMagneticFluxDensity. :param discretize.base.BaseMesh mesh: mesh - :param SimPEG.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ knownFields = {"bSolution": "F"} @@ -722,7 +722,7 @@ def _bDeriv_m(self, src, v, adjoint=False): :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the magnetic flux density derivative with respect to the inversion model with a vector """ @@ -957,7 +957,7 @@ class Fields3DCurrentDensity(FieldsFDEM): Fields object for Simulation3DCurrentDensity. :param discretize.base.BaseMesh mesh: mesh - :param SimPEG.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ knownFields = {"jSolution": "F"} @@ -1064,7 +1064,7 @@ def _jDeriv_m(self, src, v, adjoint=False): :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the current density derivative with respect to the inversion model with a vector """ @@ -1348,7 +1348,7 @@ class Fields3DMagneticField(FieldsFDEM): Fields object for Simulation3DMagneticField. :param discretize.base.BaseMesh mesh: mesh - :param SimPEG.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ knownFields = {"hSolution": "E"} @@ -1441,7 +1441,7 @@ def _hDeriv_m(self, src, v, adjoint=False): :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the magnetic field derivative with respect to the inversion model with a vector """ diff --git a/SimPEG/electromagnetics/frequency_domain/receivers.py b/simpeg/electromagnetics/frequency_domain/receivers.py similarity index 97% rename from SimPEG/electromagnetics/frequency_domain/receivers.py rename to simpeg/electromagnetics/frequency_domain/receivers.py index a28c840ff3..b424135a20 100644 --- a/SimPEG/electromagnetics/frequency_domain/receivers.py +++ b/simpeg/electromagnetics/frequency_domain/receivers.py @@ -172,11 +172,11 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetic.frequency_domain.fields.FieldsFDEM The solution for the fields defined on the mesh Returns @@ -196,11 +196,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetic.frequency_domain.fields.FieldsFDEM The solution for the fields defined on the mesh du_dm_v : numpy.ndarray The derivative of the fields on the mesh with respect to the model, diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py similarity index 98% rename from SimPEG/electromagnetics/frequency_domain/simulation.py rename to simpeg/electromagnetics/frequency_domain/simulation.py index fd85d50ddb..c68b8f3647 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -83,7 +83,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey + simpeg.electromagnetics.frequency_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -192,7 +192,7 @@ def Jvec(self, m, v, f=None): :param numpy.ndarray m: inversion model (nP,) :param numpy.ndarray v: vector which we take sensitivity product with (nP,) - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object :rtype: numpy.ndarray :return: Jv (ndata,) """ @@ -221,7 +221,7 @@ def Jtvec(self, m, v, f=None): :param numpy.ndarray m: inversion model (nP,) :param numpy.ndarray v: vector which we take adjoint product with (nP,) - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object :rtype: numpy.ndarray :return: Jv (ndata,) """ @@ -267,7 +267,7 @@ def getJ(self, m, f=None): Method to form full J given a model m :param numpy.ndarray m: inversion model (nP,) - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object :rtype: numpy.ndarray :return: J (ndata, nP) """ @@ -322,7 +322,7 @@ def getJtJdiag(self, m, W=None, f=None): :param numpy.ndarray m: inversion model (nP,) :param numpy.ndarray W: vector of weights (ndata,) - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object :rtype: numpy.ndarray :return: JtJ (nP,) """ @@ -683,7 +683,7 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): Derivative of the right hand side with respect to the model :param float freq: frequency - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/SimPEG/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py similarity index 100% rename from SimPEG/electromagnetics/frequency_domain/simulation_1d.py rename to simpeg/electromagnetics/frequency_domain/simulation_1d.py diff --git a/SimPEG/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py similarity index 98% rename from SimPEG/electromagnetics/frequency_domain/sources.py rename to simpeg/electromagnetics/frequency_domain/sources.py index 95f3c43305..dc88bc5922 100644 --- a/SimPEG/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -26,7 +26,7 @@ class BaseFDEMSrc(BaseEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -209,7 +209,7 @@ class RawVec_e(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -247,7 +247,7 @@ class RawVec_m(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -284,7 +284,7 @@ class RawVec(RawVec_e, RawVec_m): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -362,7 +362,7 @@ class MagDipole(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -909,7 +909,7 @@ class PrimSecMappedSigma(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receiver.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receiver.BaseRx List of FDEM receivers frequency : float Frequency @@ -1142,7 +1142,7 @@ def s_e(self, simulation, f=None): ---------- simulation : BaseFDEMSimulation SimPEG FDEM simulation - f : SimPEG.electromagnetics.frequency_domain.field.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.field.FieldsFDEM A SimPEG FDEM fields object Returns diff --git a/SimPEG/electromagnetics/frequency_domain/survey.py b/simpeg/electromagnetics/frequency_domain/survey.py similarity index 97% rename from SimPEG/electromagnetics/frequency_domain/survey.py rename to simpeg/electromagnetics/frequency_domain/survey.py index 4df3a90f05..c7c0a1d453 100644 --- a/SimPEG/electromagnetics/frequency_domain/survey.py +++ b/simpeg/electromagnetics/frequency_domain/survey.py @@ -8,7 +8,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.frequency_domain.sources.BaseFDEMSrc + source_list : list of simpeg.electromagnetic.frequency_domain.sources.BaseFDEMSrc List of SimPEG FDEM sources """ diff --git a/SimPEG/electromagnetics/natural_source/__init__.py b/simpeg/electromagnetics/natural_source/__init__.py similarity index 92% rename from SimPEG/electromagnetics/natural_source/__init__.py rename to simpeg/electromagnetics/natural_source/__init__.py index ceaee93eaa..dca9d80cc5 100644 --- a/SimPEG/electromagnetics/natural_source/__init__.py +++ b/simpeg/electromagnetics/natural_source/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================== -Natural Source EM (:mod:`SimPEG.electromagnetics.natural_source`) +Natural Source EM (:mod:`simpeg.electromagnetics.natural_source`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.natural_source +.. currentmodule:: simpeg.electromagnetics.natural_source About ``natural_source`` diff --git a/SimPEG/electromagnetics/natural_source/fields.py b/simpeg/electromagnetics/natural_source/fields.py similarity index 97% rename from SimPEG/electromagnetics/natural_source/fields.py rename to simpeg/electromagnetics/natural_source/fields.py index 298671aa6d..cb43b4296e 100644 --- a/SimPEG/electromagnetics/natural_source/fields.py +++ b/simpeg/electromagnetics/natural_source/fields.py @@ -233,7 +233,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ Partial derivative of the total electric field with respect to the solution. - :param SimPEG.EM.NSEM.Src src: source + :param simpeg.EM.NSEM.Src src: source :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? @@ -247,10 +247,10 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Partial derivative of the total electric field with respect to the inversion model. Here, we assume that the primary does not depend on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the electric field derivative with respect to the inversion model with a vector """ @@ -285,7 +285,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of the magnetic flux density with respect to the solution - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -303,7 +303,7 @@ def _bDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -604,7 +604,7 @@ def _e_pxDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of e_px wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -621,7 +621,7 @@ def _e_pyDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of e_py wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -639,7 +639,7 @@ def _e_pxDeriv_m(self, src, v, adjoint=False): """ Derivative of e_px wrt m - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -654,7 +654,7 @@ def _e_pyDeriv_m(self, src, v, adjoint=False): """ Derivative of e_py wrt m - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -689,7 +689,7 @@ def _b_pxDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of b_px with wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with. Size (nF,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -706,7 +706,7 @@ def _b_pxDeriv_u(self, src, du_dm_v, adjoint=False): def _b_pyDeriv_u(self, src, du_dm_v, adjoint=False): """Derivative of b_py with wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with. Size (nF,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/SimPEG/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py similarity index 98% rename from SimPEG/electromagnetics/natural_source/receivers.py rename to simpeg/electromagnetics/natural_source/receivers.py index 65aa483dc3..1bc5bda13d 100644 --- a/SimPEG/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -381,11 +381,11 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh mesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source return_complex : bool (optional) Flag for return the complex evaluation @@ -411,11 +411,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - str : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source du_dm_v : None, Supply pre-computed derivative? diff --git a/SimPEG/electromagnetics/natural_source/simulation.py b/simpeg/electromagnetics/natural_source/simulation.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/simulation.py rename to simpeg/electromagnetics/natural_source/simulation.py diff --git a/SimPEG/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/simulation_1d.py rename to simpeg/electromagnetics/natural_source/simulation_1d.py diff --git a/SimPEG/electromagnetics/natural_source/sources.py b/simpeg/electromagnetics/natural_source/sources.py similarity index 93% rename from SimPEG/electromagnetics/natural_source/sources.py rename to simpeg/electromagnetics/natural_source/sources.py index 11068a1d5c..83aeed5742 100644 --- a/SimPEG/electromagnetics/natural_source/sources.py +++ b/simpeg/electromagnetics/natural_source/sources.py @@ -19,7 +19,7 @@ class Planewave(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of NSEM receivers frequency : float Source frequency @@ -38,7 +38,7 @@ class PlanewaveXYPrimary(Planewave): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of NSEM receivers frequency : float Source frequency @@ -96,7 +96,7 @@ def ePrimary(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.natural_source_simulation.BaseNSEMSimulation + simulation : simpeg.electromagnetics.natural_source_simulation.BaseNSEMSimulation A NSEM simulation Returns @@ -116,7 +116,7 @@ def bPrimary(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation Returns @@ -138,7 +138,7 @@ def s_e(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation Returns @@ -168,7 +168,7 @@ def s_eDeriv(self, simulation, v, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation v : numpy.ndarray A vector @@ -188,7 +188,7 @@ def s_eDeriv_m(self, simulation, v, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation v : numpy.ndarray A vector diff --git a/SimPEG/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py similarity index 97% rename from SimPEG/electromagnetics/natural_source/survey.py rename to simpeg/electromagnetics/natural_source/survey.py index 3415b212ec..935316a955 100644 --- a/SimPEG/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -20,7 +20,7 @@ class Data(BaseData, DataNSEMPlotMethods): Parameters ---------- - survey : SimPEG.survey.Survey + survey : simpeg.survey.Survey Natural source EM survey dobs : numpy.ndarray Observed data @@ -143,11 +143,11 @@ def fromRecArray(cls, recArray, srcType="primary"): recArray : numpy.ndarray Record array with the data. Has to have ('freq','x','y','z') columns and some ('zxx','zxy','zyx','zyy','tzx','tzy') srcType : str, default: "primary" - The type of SimPEG.EM.NSEM.SrcNSEM to be used. Either "primary" or "total" + The type of simpeg.EM.NSEM.SrcNSEM to be used. Either "primary" or "total" Returns ------- - SimPEG.electromagnetics.natural_source.sources.SrcNSEM + simpeg.electromagnetics.natural_source.sources.SrcNSEM Natural source """ if srcType == "primary": @@ -220,7 +220,7 @@ def fromRecArray(cls, recArray, srcType="primary"): def _rec_to_ndarr(rec_arr, data_type=float): """ Function to transform a numpy record array to a nd array. - dupe of SimPEG.electromagnetics.natural_source.utils.rec_to_ndarr to avoid circular import + dupe of simpeg.electromagnetics.natural_source.utils.rec_to_ndarr to avoid circular import """ # fix for numpy >= 1.16.0 # https://numpy.org/devdocs/release/1.16.0-notes.html#multi-field-views-return-a-view-instead-of-a-copy diff --git a/SimPEG/electromagnetics/natural_source/utils/__init__.py b/simpeg/electromagnetics/natural_source/utils/__init__.py similarity index 95% rename from SimPEG/electromagnetics/natural_source/utils/__init__.py rename to simpeg/electromagnetics/natural_source/utils/__init__.py index 79425e954f..285ec2731b 100644 --- a/SimPEG/electromagnetics/natural_source/utils/__init__.py +++ b/simpeg/electromagnetics/natural_source/utils/__init__.py @@ -1,4 +1,4 @@ -""" module SimPEG.EM.NSEM.Utils +""" module simpeg.EM.NSEM.Utils Collection of utilities that are usefull for the NSEM problem diff --git a/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py b/simpeg/electromagnetics/natural_source/utils/analytic_1d.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/analytic_1d.py rename to simpeg/electromagnetics/natural_source/utils/analytic_1d.py diff --git a/SimPEG/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py similarity index 98% rename from SimPEG/electromagnetics/natural_source/utils/data_utils.py rename to simpeg/electromagnetics/natural_source/utils/data_utils.py index 3b5975de9e..6c1783428f 100644 --- a/SimPEG/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -3,14 +3,14 @@ import numpy.lib.recfunctions as recFunc from scipy.constants import mu_0 -import SimPEG as simpeg -from SimPEG.electromagnetics.natural_source.survey import Survey, Data -from SimPEG.electromagnetics.natural_source.receivers import ( +import simpeg as simpeg +from simpeg.electromagnetics.natural_source.survey import Survey, Data +from simpeg.electromagnetics.natural_source.receivers import ( PointNaturalSource, Point3DTipper, ) -from SimPEG.electromagnetics.natural_source.sources import PlanewaveXYPrimary -from SimPEG.electromagnetics.natural_source.utils import ( +from simpeg.electromagnetics.natural_source.sources import PlanewaveXYPrimary +from simpeg.electromagnetics.natural_source.utils import ( analytic_1d, plot_data_types as pDt, ) diff --git a/SimPEG/electromagnetics/natural_source/utils/data_viewer.py b/simpeg/electromagnetics/natural_source/utils/data_viewer.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/data_viewer.py rename to simpeg/electromagnetics/natural_source/utils/data_viewer.py diff --git a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py b/simpeg/electromagnetics/natural_source/utils/edi_files_utils.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py rename to simpeg/electromagnetics/natural_source/utils/edi_files_utils.py index d70c19429a..6ae02aea9e 100644 --- a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/edi_files_utils.py @@ -1,5 +1,5 @@ # Functions to import and export MT EDI files. -from SimPEG import mkvc +from simpeg import mkvc from numpy.lib import recfunctions as recFunc from .data_utils import rec_to_ndarr from discretize.utils import requires diff --git a/SimPEG/electromagnetics/natural_source/utils/plot_data_types.py b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/plot_data_types.py rename to simpeg/electromagnetics/natural_source/utils/plot_data_types.py diff --git a/SimPEG/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/plot_utils.py rename to simpeg/electromagnetics/natural_source/utils/plot_utils.py diff --git a/SimPEG/electromagnetics/natural_source/utils/solutions_1d.py b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/solutions_1d.py rename to simpeg/electromagnetics/natural_source/utils/solutions_1d.py diff --git a/SimPEG/electromagnetics/natural_source/utils/source_utils.py b/simpeg/electromagnetics/natural_source/utils/source_utils.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/utils/source_utils.py rename to simpeg/electromagnetics/natural_source/utils/source_utils.py index 64097ee4b0..7687ff5894 100644 --- a/SimPEG/electromagnetics/natural_source/utils/source_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/source_utils.py @@ -80,7 +80,7 @@ def analytic1DModelSource(mesh, freq, sigma_1d): :return: eBG_bp, E fields for the background model at both polarizations with shape (mesh.nE, 2). """ - from SimPEG.NSEM.Utils import getEHfields + from simpeg.NSEM.Utils import getEHfields # Get a 1d solution for a halfspace background if mesh.dim == 1: diff --git a/SimPEG/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/utils/test_utils.py rename to simpeg/electromagnetics/natural_source/utils/test_utils.py index 577ae25c1b..9cde461e5a 100644 --- a/SimPEG/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -1,7 +1,7 @@ import numpy as np import discretize -from SimPEG import maps, mkvc, utils, Data +from simpeg import maps, mkvc, utils, Data from ....utils import unpack_widths from ..receivers import ( PointNaturalSource, diff --git a/SimPEG/electromagnetics/static/__init__.py b/simpeg/electromagnetics/static/__init__.py similarity index 100% rename from SimPEG/electromagnetics/static/__init__.py rename to simpeg/electromagnetics/static/__init__.py diff --git a/SimPEG/electromagnetics/static/induced_polarization/__init__.py b/simpeg/electromagnetics/static/induced_polarization/__init__.py similarity index 83% rename from SimPEG/electromagnetics/static/induced_polarization/__init__.py rename to simpeg/electromagnetics/static/induced_polarization/__init__.py index b421d9afdb..d185ffd542 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/__init__.py +++ b/simpeg/electromagnetics/static/induced_polarization/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -Induced Polarization (:mod:`SimPEG.electromagnetics.static.induced_polarization`) +Induced Polarization (:mod:`simpeg.electromagnetics.static.induced_polarization`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.induced_polarization +.. currentmodule:: simpeg.electromagnetics.static.induced_polarization Simulations @@ -18,7 +18,7 @@ Receivers, Sources, and Surveys =============================== The ``induced_polarization`` module makes use of receivers, sources, and surveys -defined in the ``SimPEG.electromagnetics.static.resistivity`` module. +defined in the ``simpeg.electromagnetics.static.resistivity`` module. """ from .simulation import ( diff --git a/SimPEG/electromagnetics/static/induced_polarization/run.py b/simpeg/electromagnetics/static/induced_polarization/run.py similarity index 98% rename from SimPEG/electromagnetics/static/induced_polarization/run.py rename to simpeg/electromagnetics/static/induced_polarization/run.py index a7372118b8..d638047cf8 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/run.py +++ b/simpeg/electromagnetics/static/induced_polarization/run.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py similarity index 100% rename from SimPEG/electromagnetics/static/induced_polarization/simulation.py rename to simpeg/electromagnetics/static/induced_polarization/simulation.py diff --git a/SimPEG/electromagnetics/static/induced_polarization/survey.py b/simpeg/electromagnetics/static/induced_polarization/survey.py similarity index 100% rename from SimPEG/electromagnetics/static/induced_polarization/survey.py rename to simpeg/electromagnetics/static/induced_polarization/survey.py diff --git a/SimPEG/electromagnetics/static/resistivity/IODC.py b/simpeg/electromagnetics/static/resistivity/IODC.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/IODC.py rename to simpeg/electromagnetics/static/resistivity/IODC.py diff --git a/SimPEG/electromagnetics/static/resistivity/__init__.py b/simpeg/electromagnetics/static/resistivity/__init__.py similarity index 93% rename from SimPEG/electromagnetics/static/resistivity/__init__.py rename to simpeg/electromagnetics/static/resistivity/__init__.py index 9171c0cab9..990c6367a4 100644 --- a/SimPEG/electromagnetics/static/resistivity/__init__.py +++ b/simpeg/electromagnetics/static/resistivity/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -DC Resistivity (:mod:`SimPEG.electromagnetics.static.resistivity`) +DC Resistivity (:mod:`simpeg.electromagnetics.static.resistivity`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.resistivity +.. currentmodule:: simpeg.electromagnetics.static.resistivity Simulations diff --git a/SimPEG/electromagnetics/static/resistivity/fields.py b/simpeg/electromagnetics/static/resistivity/fields.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/fields.py rename to simpeg/electromagnetics/static/resistivity/fields.py diff --git a/SimPEG/electromagnetics/static/resistivity/fields_2d.py b/simpeg/electromagnetics/static/resistivity/fields_2d.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/fields_2d.py rename to simpeg/electromagnetics/static/resistivity/fields_2d.py diff --git a/SimPEG/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/receivers.py rename to simpeg/electromagnetics/static/resistivity/receivers.py index e41a080602..df3009a4dc 100644 --- a/SimPEG/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -162,7 +162,7 @@ def eval(self, src, mesh, f): # noqa: A003 A DC/IP source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.fields.FieldsDC + f : simpeg.electromagnetic.static.fields.FieldsDC The solution for the fields defined on the mesh Returns @@ -202,7 +202,7 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.resistivity.fields.FieldsDC + f : simpeg.electromagnetic.static.resistivity.fields.FieldsDC The solution for the fields defined on the mesh du_dm_v : numpy.ndarray, default = ``None`` The derivative of the fields on the mesh with respect to the model, diff --git a/SimPEG/electromagnetics/static/resistivity/run.py b/simpeg/electromagnetics/static/resistivity/run.py similarity index 98% rename from SimPEG/electromagnetics/static/resistivity/run.py rename to simpeg/electromagnetics/static/resistivity/run.py index 0ee948ea48..1c18666f29 100644 --- a/SimPEG/electromagnetics/static/resistivity/run.py +++ b/simpeg/electromagnetics/static/resistivity/run.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, diff --git a/SimPEG/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/simulation.py rename to simpeg/electromagnetics/static/resistivity/simulation.py diff --git a/SimPEG/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/simulation_1d.py rename to simpeg/electromagnetics/static/resistivity/simulation_1d.py diff --git a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/simulation_2d.py rename to simpeg/electromagnetics/static/resistivity/simulation_2d.py index 3d7423790d..163ef59774 100644 --- a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -28,7 +28,7 @@ class BaseDCSimulation2D(BaseElectricalPDESimulation): Base 2.5D DC problem """ - fieldsPair = Fields2D # SimPEG.EM.Static.Fields_2D + fieldsPair = Fields2D # simpeg.EM.Static.Fields_2D fieldsPair_fwd = FieldsDC # there's actually nT+1 fields, so we don't need to store the last one _mini_survey = None diff --git a/SimPEG/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/sources.py rename to simpeg/electromagnetics/static/resistivity/sources.py diff --git a/SimPEG/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/survey.py rename to simpeg/electromagnetics/static/resistivity/survey.py index cb02a32e65..8f2e438664 100644 --- a/SimPEG/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -7,7 +7,7 @@ from . import receivers as Rx from . import sources as Src from ..utils import static_utils -from SimPEG import data +from simpeg import data class Survey(BaseSurvey): @@ -15,7 +15,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.static.resistivity.sources.BaseSrc + source_list : list of simpeg.electromagnetic.static.resistivity.sources.BaseSrc List of SimPEG DC/IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. diff --git a/SimPEG/electromagnetics/static/resistivity/utils.py b/simpeg/electromagnetics/static/resistivity/utils.py similarity index 96% rename from SimPEG/electromagnetics/static/resistivity/utils.py rename to simpeg/electromagnetics/static/resistivity/utils.py index 77b0a24295..3bdec534dd 100644 --- a/SimPEG/electromagnetics/static/resistivity/utils.py +++ b/simpeg/electromagnetics/static/resistivity/utils.py @@ -6,7 +6,7 @@ from .survey import Survey # Import geometric_factor to make it available through -# SimPEG.resistivity.utils.geometric_factor (to ensure backward compatibility) +# simpeg.resistivity.utils.geometric_factor (to ensure backward compatibility) from ..utils import geometric_factor # noqa: F401 @@ -27,7 +27,7 @@ def WennerSrcList(n_electrodes, a_spacing, in2D=False, plotIt=False): Returns ------- - list of SimPEG.electromagnetics.static.resistivity.sources.Dipole + list of simpeg.electromagnetics.static.resistivity.sources.Dipole List of sources and their associate receivers for the Wenner survey """ diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py b/simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py similarity index 91% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py index 9bdeea56be..9eefe9606d 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py @@ -1,8 +1,8 @@ """ ==================================================================================================== -Spectral Induced Polarization (:mod:`SimPEG.electromagnetics.static.induced_polarization`) +Spectral Induced Polarization (:mod:`simpeg.electromagnetics.static.induced_polarization`) ==================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.static.spectral_induced_polarization +.. currentmodule:: simpeg.electromagnetics.static.spectral_induced_polarization Simulations diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/data.py b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py similarity index 100% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/data.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/data.py diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py similarity index 99% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py index 30725aeec9..3865349fab 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py @@ -148,7 +148,7 @@ def eval(self, src, mesh, f): # noqa: A003 A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.spectral_induced_polarization.Fields + f : simpeg.electromagnetic.static.spectral_induced_polarization.Fields The solution for the fields defined on the mesh Returns @@ -176,7 +176,7 @@ def evalDeriv(self, src, mesh, f, v, adjoint=False): A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.spectral_induced_polarization.Fields + f : simpeg.electromagnetic.static.spectral_induced_polarization.Fields The solution for the fields defined on the mesh v : numpy.ndarray A vector diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py similarity index 99% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/run.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 101385a70b..93ee3cf5ed 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py similarity index 100% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/simulation.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/simulation_2d.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py similarity index 100% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/simulation_2d.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py similarity index 98% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/sources.py index 1bc1b71e34..5553833128 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py @@ -30,7 +30,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx List of receivers associated with the source """ return self._receiver_list @@ -210,7 +210,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - sim : SimPEG.electromagnetics.static.spectral_induced_polarization.simulation.BaseDCSimulation + sim : simpeg.electromagnetics.static.spectral_induced_polarization.simulation.BaseDCSimulation A spectral IP simulation Returns diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py similarity index 98% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 8d9a7932f3..4f3dbd1d27 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -10,7 +10,7 @@ class Survey(BaseTimeSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.static.spectral_induced_polarization.sources.BaseSrc + source_list : list of simpeg.electromagnetic.static.spectral_induced_polarization.sources.BaseSrc List of SimPEG spectral IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py similarity index 82% rename from SimPEG/electromagnetics/static/spontaneous_potential/__init__.py rename to simpeg/electromagnetics/static/spontaneous_potential/__init__.py index 2fcc59d552..25d32706f7 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -Spontaneous Potential (:mod:`SimPEG.electromagnetics.static.spontaneous_potential`) +Spontaneous Potential (:mod:`simpeg.electromagnetics.static.spontaneous_potential`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.spontaneous_potential +.. currentmodule:: simpeg.electromagnetics.static.spontaneous_potential Simulations @@ -14,7 +14,7 @@ Receivers ========= -This module makes use of the receivers in :mod:`SimPEG.electromagnetics.static.resistivity` +This module makes use of the receivers in :mod:`simpeg.electromagnetics.static.resistivity` Sources ======= diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/simulation.py b/simpeg/electromagnetics/static/spontaneous_potential/simulation.py similarity index 99% rename from SimPEG/electromagnetics/static/spontaneous_potential/simulation.py rename to simpeg/electromagnetics/static/spontaneous_potential/simulation.py index fdc2ff36ea..007fd282fe 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/simulation.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/simulation.py @@ -19,7 +19,7 @@ class Simulation3DCellCentered(dc.Simulation3DCellCentered): q : float, array_like, optional The charge density accumulation rate model (C/(s m^3)), also physically represents the volumetric current density (A/m^3). - qMap : SimPEG.maps.IdentityMap, optional + qMap : simpeg.maps.IdentityMap, optional The mapping used to go from the simulation model to `q`. Set this to invert for `q`. **kwargs diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/sources.py b/simpeg/electromagnetics/static/spontaneous_potential/sources.py similarity index 85% rename from SimPEG/electromagnetics/static/spontaneous_potential/sources.py rename to simpeg/electromagnetics/static/spontaneous_potential/sources.py index 33a7d8c347..22a71d8560 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/sources.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/sources.py @@ -1,7 +1,7 @@ import numpy as np -from SimPEG.electromagnetics.static.resistivity import receivers -from SimPEG import survey -from SimPEG.utils import validate_list_of_types +from simpeg.electromagnetics.static.resistivity import receivers +from simpeg import survey +from simpeg.utils import validate_list_of_types class StreamingCurrents(survey.BaseSrc): diff --git a/SimPEG/electromagnetics/static/utils/__init__.py b/simpeg/electromagnetics/static/utils/__init__.py similarity index 92% rename from SimPEG/electromagnetics/static/utils/__init__.py rename to simpeg/electromagnetics/static/utils/__init__.py index 5245be1490..e6ce7096bd 100644 --- a/SimPEG/electromagnetics/static/utils/__init__.py +++ b/simpeg/electromagnetics/static/utils/__init__.py @@ -1,8 +1,8 @@ """ ==================================================================================================== -Static Utilities (:mod:`SimPEG.electromagnetics.utils`) +Static Utilities (:mod:`simpeg.electromagnetics.utils`) ==================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.static.utils +.. currentmodule:: simpeg.electromagnetics.static.utils Electrode Utilities diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py similarity index 99% rename from SimPEG/electromagnetics/static/utils/static_utils.py rename to simpeg/electromagnetics/static/utils/static_utils.py index 573199e260..d52892c2aa 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -75,7 +75,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): Parameters ---------- - survey_object : SimPEG.electromagnetics.static.survey.Survey + survey_object : simpeg.electromagnetics.static.survey.Survey A DC or IP survey object electrode_pair : {'all', 'AB', 'MN', 'AM', 'AN', 'BM', 'BN} Which electrode separation pairs to compute. @@ -504,7 +504,7 @@ def plot_pseudosection( Parameters ---------- - data : SimPEG.electromagnetics.static.survey.Survey or SimPEG.data.Data + data : simpeg.electromagnetics.static.survey.Survey or SimPEG.data.Data A DC or IP survey object defining a 2D survey line, or a Data object containing that same type of survey object. dobs : numpy.ndarray (ndata,) or None @@ -542,7 +542,7 @@ def plot_pseudosection( An axis object for the colorbar data_type : str, optional If dobs is ``None``, this will transform the data vector in the `survey` parameter - when it is a SimPEG.data.Data object from voltage to the requested `data_type`. + when it is a simpeg.data.Data object from voltage to the requested `data_type`. This occurs when `dobs` is `None`. You may also use "apparent_conductivity" or "apparent_resistivity" to define the data type. space_type : {'half space', "whole space"} @@ -766,7 +766,7 @@ def plot_3d_pseudosection( Parameters ---------- - survey : SimPEG.electromagnetics.static.survey.Survey + survey : simpeg.electromagnetics.static.survey.Survey A DC or IP survey object dvec : numpy.ndarray A data vector containing volts, integrated chargeabilities, apparent @@ -962,7 +962,7 @@ def generate_survey_from_abmn_locations( output_sorting : bool This option is used if the ABMN locations are sorted during the creation of the survey and you would like to sort any data vectors associated with the electrode locations. - If False, the function will output a SimPEG.electromagnetic.static.survey.Survey object. + If False, the function will output a simpeg.electromagnetic.static.survey.Survey object. If True, the function will output a tuple containing the survey object and a numpy array (n,) that will sort the data vector to match the order of the electrodes in the survey. @@ -970,7 +970,7 @@ def generate_survey_from_abmn_locations( Returns ------- survey - A SimPEG.electromagnetic.static.survey.Survey object + A simpeg.electromagnetic.static.survey.Survey object sort_index A numpy array which defines any sorting that took place when creating the survey @@ -1761,7 +1761,7 @@ def gen_3d_survey_from_2d_lines( Returns ------- - SimPEG.dc.SurveyDC.Survey + simpeg.dc.SurveyDC.Survey A 3D DC survey object """ ylocs = np.arange(n_lines) * line_spacing + y0 diff --git a/SimPEG/electromagnetics/time_domain/__init__.py b/simpeg/electromagnetics/time_domain/__init__.py similarity index 94% rename from SimPEG/electromagnetics/time_domain/__init__.py rename to simpeg/electromagnetics/time_domain/__init__.py index 9c3b41ba21..3fb8db7522 100644 --- a/SimPEG/electromagnetics/time_domain/__init__.py +++ b/simpeg/electromagnetics/time_domain/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================== -Time-Domain EM (:mod:`SimPEG.electromagnetics.time_domain`) +Time-Domain EM (:mod:`simpeg.electromagnetics.time_domain`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.time_domain +.. currentmodule:: simpeg.electromagnetics.time_domain About ``time_domain`` diff --git a/SimPEG/electromagnetics/time_domain/fields.py b/simpeg/electromagnetics/time_domain/fields.py similarity index 100% rename from SimPEG/electromagnetics/time_domain/fields.py rename to simpeg/electromagnetics/time_domain/fields.py diff --git a/SimPEG/electromagnetics/time_domain/receivers.py b/simpeg/electromagnetics/time_domain/receivers.py similarity index 94% rename from SimPEG/electromagnetics/time_domain/receivers.py rename to simpeg/electromagnetics/time_domain/receivers.py index 9d6e46ccbb..61e183a4cc 100644 --- a/SimPEG/electromagnetics/time_domain/receivers.py +++ b/simpeg/electromagnetics/time_domain/receivers.py @@ -85,7 +85,7 @@ def getSpatialP(self, mesh, f): ---------- mesh : discretize.BaseMesh A discretize mesh - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -110,7 +110,7 @@ def getTimeP(self, time_mesh, f): ---------- time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -129,7 +129,7 @@ def getP(self, mesh, time_mesh, f): A discretize mesh defining spatial discretization time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -157,13 +157,13 @@ def eval(self, src, mesh, time_mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh Returns @@ -180,13 +180,13 @@ def evalDeriv(self, src, mesh, time_mesh, f, v, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh v : numpy.ndarray A vector @@ -272,13 +272,13 @@ def eval(self, src, mesh, time_mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh Returns @@ -305,7 +305,7 @@ def getTimeP(self, time_mesh, f): ---------- time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- diff --git a/SimPEG/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py similarity index 99% rename from SimPEG/electromagnetics/time_domain/simulation.py rename to simpeg/electromagnetics/time_domain/simulation.py index 09ae43a85e..edb374cf06 100644 --- a/SimPEG/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -37,7 +37,7 @@ def survey(self): """The survey for the simulation Returns ------- - SimPEG.electromagnetics.time_domain.survey.Survey + simpeg.electromagnetics.time_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -81,7 +81,7 @@ def fields(self, m): Solve the forward problem for the fields. :param numpy.ndarray m: inversion model (nP,) - :rtype: SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + :rtype: simpeg.electromagnetics.time_domain.fields.FieldsTDEM :return f: fields object """ @@ -555,7 +555,7 @@ class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): _fieldType = "b" _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity #: A SimPEG.EM.TDEM.Fields3DMagneticFluxDensity object + fieldsPair = Fields3DMagneticFluxDensity #: A simpeg.EM.TDEM.Fields3DMagneticFluxDensity object Fields_Derivs = FieldsDerivativesEB def getAdiag(self, tInd): diff --git a/SimPEG/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py similarity index 100% rename from SimPEG/electromagnetics/time_domain/simulation_1d.py rename to simpeg/electromagnetics/time_domain/simulation_1d.py diff --git a/SimPEG/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py similarity index 98% rename from SimPEG/electromagnetics/time_domain/sources.py rename to simpeg/electromagnetics/time_domain/sources.py index a3c0a64eb9..a3b63b1172 100644 --- a/SimPEG/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -156,7 +156,7 @@ class StepOffWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) >>> waveform = tdem.sources.StepOffWaveform(off_time=1e-5) @@ -189,7 +189,7 @@ class RampOffWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) >>> waveform = tdem.sources.RampOffWaveform(off_time=1e-5) @@ -245,7 +245,7 @@ class RawWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> def my_waveform(t): >>> period = 1e-2 @@ -308,7 +308,7 @@ class VTEMWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.VTEMWaveform() @@ -412,7 +412,7 @@ class TrapezoidWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.TrapezoidWaveform(ramp_on=[0.0, 2e-3], ramp_off=[4e-3, 6e-3]) @@ -518,7 +518,7 @@ class TriangularWaveform(TrapezoidWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.TriangularWaveform(start_time=1E-3, off_time=6e-3, peak_time=3e-3) @@ -588,7 +588,7 @@ class QuarterSineRampOnWaveform(TrapezoidWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.QuarterSineRampOnWaveform(ramp_on=(0, 2e-3), ramp_off=(3e-3, 3.5e-3)) @@ -827,7 +827,7 @@ class ExponentialWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(-1e-2, 1e-2, 1000) >>> waveform = tdem.sources.ExponentialWaveform() @@ -954,7 +954,7 @@ class BaseTDEMSrc(BaseEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers location : (dim) numpy.ndarray Source locations @@ -1060,7 +1060,7 @@ def eval(self, simulation, time): # noqa: A003 Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseTDEMSimulation + simulation : simpeg.electromagnetics.base.BaseTDEMSimulation An instance of a time-domain electromagnetic simulation time : float The time at which you want to compute the source terms @@ -1080,7 +1080,7 @@ def evalDeriv(self, simulation, time, v=None, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseTDEMSimulation + simulation : simpeg.electromagnetics.base.BaseTDEMSimulation An instance of a time-domain electromagnetic simulation time : The time at which you want to compute the derivative @@ -1133,7 +1133,7 @@ class MagDipole(BaseTDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx A list of TDEM receivers location : (dim) numpy.ndarray, default = np.r_[0., 0., 0.] Source location. @@ -1458,7 +1458,7 @@ class CircularLoop(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx A list of TDEM receivers location : (dim) np.ndarray, default = np.r_[0., 0., 0.] Source location. @@ -1613,7 +1613,7 @@ class LineCurrent(BaseTDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx List of TDEM receivers location : (n, 3) numpy.ndarray Array defining the node locations for the wire path. For inductive sources, @@ -1700,7 +1700,7 @@ def Mejs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns diff --git a/SimPEG/electromagnetics/time_domain/survey.py b/simpeg/electromagnetics/time_domain/survey.py similarity index 93% rename from SimPEG/electromagnetics/time_domain/survey.py rename to simpeg/electromagnetics/time_domain/survey.py index e8798d8048..946d22e823 100644 --- a/SimPEG/electromagnetics/time_domain/survey.py +++ b/simpeg/electromagnetics/time_domain/survey.py @@ -13,7 +13,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.time_domain.sources.BaseTDEMSrc + source_list : list of simpeg.electromagnetic.time_domain.sources.BaseTDEMSrc List of SimPEG TDEM sources """ diff --git a/SimPEG/electromagnetics/utils/__init__.py b/simpeg/electromagnetics/utils/__init__.py similarity index 87% rename from SimPEG/electromagnetics/utils/__init__.py rename to simpeg/electromagnetics/utils/__init__.py index d8d4a6182c..bf7fd197b9 100644 --- a/SimPEG/electromagnetics/utils/__init__.py +++ b/simpeg/electromagnetics/utils/__init__.py @@ -1,9 +1,9 @@ """ =================================================================== -Electromagnetics Utilities (:mod:`SimPEG.electromagnetics.utils`) +Electromagnetics Utilities (:mod:`simpeg.electromagnetics.utils`) =================================================================== -.. currentmodule:: SimPEG.electromagnetics.utils +.. currentmodule:: simpeg.electromagnetics.utils Current Utilities diff --git a/SimPEG/electromagnetics/utils/current_utils.py b/simpeg/electromagnetics/utils/current_utils.py similarity index 100% rename from SimPEG/electromagnetics/utils/current_utils.py rename to simpeg/electromagnetics/utils/current_utils.py diff --git a/SimPEG/electromagnetics/utils/em1d_utils.py b/simpeg/electromagnetics/utils/em1d_utils.py similarity index 99% rename from SimPEG/electromagnetics/utils/em1d_utils.py rename to simpeg/electromagnetics/utils/em1d_utils.py index 21a08dbd6a..91b0ee3f61 100644 --- a/SimPEG/electromagnetics/utils/em1d_utils.py +++ b/simpeg/electromagnetics/utils/em1d_utils.py @@ -2,7 +2,7 @@ from geoana.em.fdem.base import skin_depth from geoana.em.tdem import diffusion_distance -from SimPEG import utils +from simpeg import utils def get_vertical_discretization(n_layer, minimum_dz, geomtric_factor): diff --git a/SimPEG/electromagnetics/utils/testing_utils.py b/simpeg/electromagnetics/utils/testing_utils.py similarity index 98% rename from SimPEG/electromagnetics/utils/testing_utils.py rename to simpeg/electromagnetics/utils/testing_utils.py index 3315c71061..ec7ad01a33 100644 --- a/SimPEG/electromagnetics/utils/testing_utils.py +++ b/simpeg/electromagnetics/utils/testing_utils.py @@ -4,8 +4,8 @@ from discretize import TensorMesh from ... import maps, utils -from SimPEG import SolverLU -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import SolverLU +from simpeg.electromagnetics import frequency_domain as fdem FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order CONDUCTIVITY = 1e1 diff --git a/SimPEG/electromagnetics/utils/waveform_utils.py b/simpeg/electromagnetics/utils/waveform_utils.py similarity index 98% rename from SimPEG/electromagnetics/utils/waveform_utils.py rename to simpeg/electromagnetics/utils/waveform_utils.py index 7166eb62f9..55e2c2f8ab 100644 --- a/SimPEG/electromagnetics/utils/waveform_utils.py +++ b/simpeg/electromagnetics/utils/waveform_utils.py @@ -83,7 +83,7 @@ def convolve_with_waveform(func, waveform, times, fargs=None, fkwargs=None): ---------- func : callable function of `t` that should be convolved - waveform : SimPEG.electromagnetics.time_domain.waveforms.BaseWaveform + waveform : simpeg.electromagnetics.time_domain.waveforms.BaseWaveform times : array_like fargs : list, optional extra arguments given to `func` diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py b/simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py similarity index 91% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py index 2f3f05c1f8..76379773ec 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py @@ -1,8 +1,8 @@ """ =========================================================================================================== -Viscous Remanent Magnetization (:mod:`SimPEG.electromagnetics.viscous_remanent_magnetization`) +Viscous Remanent Magnetization (:mod:`simpeg.electromagnetics.viscous_remanent_magnetization`) =========================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.viscous_remanent_magnetization +.. currentmodule:: simpeg.electromagnetics.viscous_remanent_magnetization About ``viscous_remanent_magnetization`` diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/receivers.py b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py similarity index 100% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/receivers.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py similarity index 99% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index 35ead8fd9b..27cfb040f1 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -69,7 +69,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.viscous_temanent_magnetization.survey.Survey + simpeg.electromagnetics.viscous_temanent_magnetization.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey.") diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py similarity index 99% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/sources.py index db332af0a7..5ddd369e98 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py @@ -16,7 +16,7 @@ class BaseSrcVRM(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point A list of VRM receivers location : (3) array_like Source location diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py b/simpeg/electromagnetics/viscous_remanent_magnetization/survey.py similarity index 97% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/survey.py index e2bfba0197..0f65d97d27 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/survey.py @@ -16,7 +16,7 @@ class SurveyVRM(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.viscous_remanent_magnetization.sources.BaseSrcVRM + source_list : list of simpeg.electromagnetic.viscous_remanent_magnetization.sources.BaseSrcVRM List of SimPEG VRM sources t_active : numpy.ndarray of bool Active time channels used in inversion diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/waveforms.py b/simpeg/electromagnetics/viscous_remanent_magnetization/waveforms.py similarity index 100% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/waveforms.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/waveforms.py diff --git a/SimPEG/fields.py b/simpeg/fields.py similarity index 99% rename from SimPEG/fields.py rename to simpeg/fields.py index dcd5bfb8c3..d3eb461c27 100644 --- a/SimPEG/fields.py +++ b/simpeg/fields.py @@ -49,7 +49,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseSimulation + simpeg.simulation.BaseSimulation """ return self._simulation @@ -287,7 +287,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseTimeSimulation + simpeg.simulation.BaseTimeSimulation """ return self._simulation diff --git a/SimPEG/flow/__init__.py b/simpeg/flow/__init__.py similarity index 100% rename from SimPEG/flow/__init__.py rename to simpeg/flow/__init__.py diff --git a/SimPEG/flow/richards/__init__.py b/simpeg/flow/richards/__init__.py similarity index 92% rename from SimPEG/flow/richards/__init__.py rename to simpeg/flow/richards/__init__.py index ec638e997f..d195f6df1e 100644 --- a/SimPEG/flow/richards/__init__.py +++ b/simpeg/flow/richards/__init__.py @@ -1,8 +1,8 @@ """ ======================================================================= -Richards Flow (:mod:`SimPEG.flow.richards`) +Richards Flow (:mod:`simpeg.flow.richards`) ======================================================================= -.. currentmodule:: SimPEG.flow.richards +.. currentmodule:: simpeg.flow.richards About ``Richards flow`` diff --git a/SimPEG/flow/richards/empirical.py b/simpeg/flow/richards/empirical.py similarity index 99% rename from SimPEG/flow/richards/empirical.py rename to simpeg/flow/richards/empirical.py index 83b38f33f0..d726aaad91 100644 --- a/SimPEG/flow/richards/empirical.py +++ b/simpeg/flow/richards/empirical.py @@ -33,8 +33,8 @@ def _partition_args(mesh, Hcond, Theta, hcond_args, theta_args, **kwargs): class NonLinearModel(props.HasModel): """A non linear model that has dependence on the fields and a model""" - counter = None #: A SimPEG.utils.Counter object - mesh = None #: A SimPEG Mesh + counter = None #: A simpeg.utils.Counter object + mesh = None #: A discretize Mesh def __init__(self, mesh, **kwargs): self.mesh = mesh diff --git a/SimPEG/flow/richards/receivers.py b/simpeg/flow/richards/receivers.py similarity index 96% rename from SimPEG/flow/richards/receivers.py rename to simpeg/flow/richards/receivers.py index 5a2d16b109..fc815a6434 100644 --- a/SimPEG/flow/richards/receivers.py +++ b/simpeg/flow/richards/receivers.py @@ -23,7 +23,7 @@ def deriv(self, U, simulation, du_dm_v=None, v=None, adjoint=False): ---------- U : (n_time) list of (n_cells) numpy.ndarray Fields computed on the mesh. This is unused for this receiver. - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flor simulation du_dm_v : numpy.ndarray Derivative with respect to the model times a vector @@ -65,7 +65,7 @@ def deriv(self, U, simulation, du_dm_v=None, v=None, adjoint=False): ---------- U : (n_time) list of (n_cells) numpy.ndarray Fields computed on the mesh. This is unused for this receiver. - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flor simulation du_dm_v : numpy.ndarray Derivative with respect to the model times a vector diff --git a/SimPEG/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py similarity index 100% rename from SimPEG/flow/richards/simulation.py rename to simpeg/flow/richards/simulation.py diff --git a/SimPEG/flow/richards/survey.py b/simpeg/flow/richards/survey.py similarity index 93% rename from SimPEG/flow/richards/survey.py rename to simpeg/flow/richards/survey.py index 1f6848088d..5bf8a47ba3 100644 --- a/SimPEG/flow/richards/survey.py +++ b/simpeg/flow/richards/survey.py @@ -18,7 +18,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.survey.BaseRx + list of simpeg.survey.BaseRx List of receivers associated with the survey """ return self._receiver_list @@ -43,7 +43,7 @@ def deriv(self, simulation, f, du_dm_v=None, v=None): Parameters ---------- - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flow simulation class f : (n_times) list of numpy.ndarray Fields @@ -68,7 +68,7 @@ def derivAdjoint(self, simulation, f, v=None): Parameters ---------- - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flow simulation class f : (n_times) list of numpy.ndarray Fields. diff --git a/SimPEG/inverse_problem.py b/simpeg/inverse_problem.py similarity index 95% rename from SimPEG/inverse_problem.py rename to simpeg/inverse_problem.py index bafe05e4ee..b4f5630bd9 100644 --- a/SimPEG/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -71,11 +71,11 @@ def debug(self, value): @property def counter(self): - """Set this to a `SimPEG.utils.Counter` if you want to count things. + """Set this to a `simpeg.utils.Counter` if you want to count things. Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter """ return self._counter @@ -91,7 +91,7 @@ def dmisfit(self): Returns ------- - SimPEG.objective_function.ComboObjectiveFunction + simpeg.objective_function.ComboObjectiveFunction """ return self._dmisfit @@ -108,7 +108,7 @@ def reg(self): Returns ------- - SimPEG.objective_function.ComboObjectiveFunction + simpeg.objective_function.ComboObjectiveFunction """ return self._reg @@ -125,7 +125,7 @@ def opt(self): Returns ------- - SimPEG.optimization.Minimize + simpeg.optimization.Minimize """ return self._opt @@ -180,7 +180,7 @@ def startup(self, m0): and getattr(fct, "reference_model", None) is None ): print( - "SimPEG.InvProblem will set Regularization.reference_model to m0." + "simpeg.InvProblem will set Regularization.reference_model to m0." ) fct.reference_model = m0 @@ -200,7 +200,7 @@ def startup(self, m0): solver_opts = objfct.simulation.solver_opts print( """ - SimPEG.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. + simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. ***Done using same Solver, and solver_opts as the {} problem*** """.format( objfct.simulation.__class__.__name__ @@ -211,7 +211,7 @@ def startup(self, m0): if set_default: print( """ - SimPEG.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. + simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. ***Done using the default solver {} and no solver_opts.*** """.format( DefaultSolver.__name__ diff --git a/SimPEG/inversion.py b/simpeg/inversion.py similarity index 96% rename from SimPEG/inversion.py rename to simpeg/inversion.py index 09245631c0..9826f8cca9 100644 --- a/SimPEG/inversion.py +++ b/simpeg/inversion.py @@ -67,14 +67,14 @@ def debug(self): def debug(self, value): self._debug = validate_type("debug", value, bool) - #: Set this to a SimPEG.utils.Counter() if you want to count things + #: Set this to a simpeg.utils.Counter() if you want to count things @property def counter(self): """The counter. Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter """ return self._counter diff --git a/SimPEG/maps.py b/simpeg/maps.py similarity index 99% rename from SimPEG/maps.py rename to simpeg/maps.py index f4530aefff..8e9cfc7bc8 100644 --- a/SimPEG/maps.py +++ b/simpeg/maps.py @@ -258,7 +258,7 @@ def __mul__(self, val): ) def dot(self, map1): - r"""Multiply two mappings to create a :class:`SimPEG.maps.ComboMap`. + r"""Multiply two mappings to create a :class:`simpeg.maps.ComboMap`. Let :math:`\mathbf{f}_1` and :math:`\mathbf{f}_2` represent two mapping functions. Where :math:`\mathbf{m}` represents a set of input model parameters, @@ -289,7 +289,7 @@ def dot(self, map1): a vector space of length 5, then takes the natural exponent. >>> import numpy as np - >>> from SimPEG.maps import ExpMap, Projection + >>> from simpeg.maps import ExpMap, Projection >>> nP1 = 1 >>> nP2 = 5 @@ -378,7 +378,7 @@ class ComboMap(IdentityMap): Parameters ---------- - maps : list of SimPEG.maps.IdentityMap + maps : list of simpeg.maps.IdentityMap A ``list`` of SimPEG mapping objects. The ordering of the mapping objects in the ``list`` is from last applied to first applied! @@ -388,7 +388,7 @@ class ComboMap(IdentityMap): a vector space of length 5, then takes the natural exponent. >>> import numpy as np - >>> from SimPEG.maps import ExpMap, Projection, ComboMap + >>> from simpeg.maps import ExpMap, Projection, ComboMap >>> nP1 = 1 >>> nP2 = 5 @@ -655,7 +655,7 @@ class Projection(IdentityMap): Here we define a mapping that rearranges and projects 2 model parameters to a vector space spanning 4 parameters. - >>> from SimPEG.maps import Projection + >>> from simpeg.maps import Projection >>> import numpy as np >>> nP = 2 @@ -895,7 +895,7 @@ class SurjectUnits(IdentityMap): all cells whose centers are located at *x < 0* and the 2nd unit's value is assigned to all cells whose centers are located at *x > 0*. - >>> from SimPEG.maps import SurjectUnits + >>> from simpeg.maps import SurjectUnits >>> from discretize import TensorMesh >>> import numpy as np @@ -1277,7 +1277,7 @@ class Wires(object): are two parameters types. Note that the number of parameters of each type does not need to be the same. - >>> from SimPEG.maps import Wires, ReciprocalMap + >>> from simpeg.maps import Wires, ReciprocalMap >>> import numpy as np >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] @@ -2679,7 +2679,7 @@ class ComplexMap(IdentityMap): (4 real and 4 imaginary values). The output of the mapping is a complex array with 4 values. - >>> from SimPEG.maps import ComplexMap + >>> from simpeg.maps import ComplexMap >>> from discretize import TensorMesh >>> import numpy as np @@ -2776,7 +2776,7 @@ def deriv(self, m, v=None): mesh comprised of 4 cells. We then demonstrate how the derivative of the mapping and its adjoint can be applied to a vector. - >>> from SimPEG.maps import ComplexMap + >>> from simpeg.maps import ComplexMap >>> from discretize import TensorMesh >>> import numpy as np @@ -2929,8 +2929,8 @@ class SurjectVertical1D(IdentityMap): construct a mapping which projects the 1D model onto a 2D tensor mesh. - >>> from SimPEG.maps import SurjectVertical1D - >>> from SimPEG.utils import plot_1d_layer_model + >>> from simpeg.maps import SurjectVertical1D + >>> from simpeg.utils import plot_1d_layer_model >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib as mpl @@ -3061,7 +3061,7 @@ class Surject2Dto3D(IdentityMap): we project the model along the y-axis to obtain a 3D distribution for the physical property (i.e. a 3D tensor model). - >>> from SimPEG.maps import Surject2Dto3D + >>> from simpeg.maps import Surject2Dto3D >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib as mpl @@ -3526,7 +3526,7 @@ class ParametricCircleMap(IdentityMap): Here we define the parameterized model for a circle in a wholespace. We then create and use a ``ParametricCircleMap`` to map the model to a 2D mesh. - >>> from SimPEG.maps import ParametricCircleMap + >>> from simpeg.maps import ParametricCircleMap >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -3804,7 +3804,7 @@ class ParametricPolyMap(IdentityMap): We then use an active cells mapping to map from the set of active cells to all cells in the 2D mesh. - >>> from SimPEG.maps import ParametricPolyMap, InjectActiveCells + >>> from simpeg.maps import ParametricPolyMap, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4162,7 +4162,7 @@ class ParametricSplineMap(IdentityMap): for the interface at the horizontal positions supplied when creating the mapping. - >>> from SimPEG.maps import ParametricSplineMap + >>> from simpeg.maps import ParametricSplineMap >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4705,7 +4705,7 @@ class ParametricLayer(BaseParametric): (i.e. below the surface), We then use an active cells mapping to map from the set of active cells to all cells in the mesh. - >>> from SimPEG.maps import ParametricLayer, InjectActiveCells + >>> from simpeg.maps import ParametricLayer, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4967,7 +4967,7 @@ class ParametricBlock(BaseParametric): set of active cells (i.e. below the surface), We then use an active cells mapping to map from the set of active cells to all cells in the mesh. - >>> from SimPEG.maps import ParametricBlock, InjectActiveCells + >>> from simpeg.maps import ParametricBlock, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -5310,7 +5310,7 @@ class ParametricEllipsoid(ParametricBlock): set of active cells (i.e. below the surface), We then use an active cells mapping to map from the set of active cells to all cells in the mesh. - >>> from SimPEG.maps import ParametricEllipsoid, InjectActiveCells + >>> from simpeg.maps import ParametricEllipsoid, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt diff --git a/SimPEG/meta/__init__.py b/simpeg/meta/__init__.py similarity index 96% rename from SimPEG/meta/__init__.py rename to simpeg/meta/__init__.py index d78117a2e4..3dca694298 100644 --- a/SimPEG/meta/__init__.py +++ b/simpeg/meta/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Meta SimPEG Classes (:mod:`SimPEG.meta`) +Meta SimPEG Classes (:mod:`simpeg.meta`) ======================================================== -.. currentmodule:: SimPEG.meta +.. currentmodule:: simpeg.meta SimPEG's meta module defines tools for working with simulations representing many smaller simulations working together to solve a geophysical problem. diff --git a/SimPEG/meta/dask_sim.py b/simpeg/meta/dask_sim.py similarity index 96% rename from SimPEG/meta/dask_sim.py rename to simpeg/meta/dask_sim.py index 268f5260dc..e52db04b56 100644 --- a/SimPEG/meta/dask_sim.py +++ b/simpeg/meta/dask_sim.py @@ -1,10 +1,10 @@ import numpy as np -from SimPEG.simulation import BaseSimulation -from SimPEG.survey import BaseSurvey -from SimPEG.maps import IdentityMap -from SimPEG.utils import validate_list_of_types, validate_type -from SimPEG.props import HasModel +from simpeg.simulation import BaseSimulation +from simpeg.survey import BaseSurvey +from simpeg.maps import IdentityMap +from simpeg.utils import validate_list_of_types, validate_type +from simpeg.props import HasModel import itertools from dask.distributed import Client from dask.distributed import Future @@ -139,10 +139,10 @@ class DaskMetaSimulation(MetaSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation or list of dask.distributed.Future + simulations : (n_sim) list of simpeg.simulation.BaseSimulation or list of dask.distributed.Future The list of unique simulations (or futures that would return a simulation) that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future The map for every simulation (or futures that would return a map). Every map should accept the same length model, and output a model appropriate for its paired simulation. @@ -170,7 +170,7 @@ def simulations(self): Returns ------- - (n_sim) list of distributed.Future SimPEG.simulation.BaseSimulation + (n_sim) list of distributed.Future simpeg.simulation.BaseSimulation """ return self._simulations @@ -192,7 +192,7 @@ def mappings(self): Returns ------- - (n_sim) list of distributed.Future SimPEG.maps.IdentityMap + (n_sim) list of distributed.Future simpeg.maps.IdentityMap """ return self._mappings @@ -445,10 +445,10 @@ class DaskSumMetaSimulation(DaskMetaSimulation, SumMetaSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation or list of dask.distributed.Future + simulations : (n_sim) list of simpeg.simulation.BaseSimulation or list of dask.distributed.Future The list of unique simulations that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future The map for every simulation. Every map should accept the + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future The map for every simulation. Every map should accept the same length model, and output a model appropriate for its paired simulation. client : dask.distributed.Client, optional @@ -588,7 +588,7 @@ class DaskRepeatedSimulation(DaskMetaSimulation): ---------- simulation : SimPEG.simulation.BaseSimulation or dask.distributed.Future The simulation to use repeatedly with different mappings. - mappings : (n_sim) list of SimPEG.maps.IdentityMap or list of dask.distributed.Future + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future The list of different mappings to use (or futures that each return a mapping). client : dask.distributed.Client, optional The dask client to use for communication. @@ -621,7 +621,7 @@ def simulation(self): Returns ------- - distributed.Future of SimPEG.simulation.BaseSimulation + distributed.Future of simpeg.simulation.BaseSimulation """ return self._simulation diff --git a/SimPEG/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py similarity index 98% rename from SimPEG/meta/multiprocessing.py rename to simpeg/meta/multiprocessing.py index bc3708d57a..56dbb2df65 100644 --- a/SimPEG/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -1,6 +1,6 @@ from multiprocessing import Process, Queue, cpu_count -from SimPEG.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation -from SimPEG.props import HasModel +from simpeg.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation +from simpeg.props import HasModel import uuid import numpy as np @@ -187,7 +187,7 @@ class MultiprocessingMetaSimulation(MetaSimulation): protecting your function calls by checking if you are in `__main__` with: - >>> from SimPEG.meta import MultiprocessingMetaSimulation + >>> from simpeg.meta import MultiprocessingMetaSimulation >>> if __name__ == '__main__': ... # Do processing here ... sim = MultiprocessingMetaSimulation(...) @@ -358,10 +358,10 @@ class MultiprocessingSumMetaSimulation( Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + simulations : (n_sim) list of simpeg.simulation.BaseSimulation The list of unique simulations that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The map for every simulation. Every map should accept the same length model, and output a model appropriate for its paired simulation. @@ -437,7 +437,7 @@ class MultiprocessingRepeatedSimulation( ---------- simulation : SimPEG.simulation.BaseSimulation The simulation to use repeatedly with different mappings. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The list of different mappings to use. n_processes : optional The number of processes to spawn internally. This will default diff --git a/SimPEG/meta/simulation.py b/simpeg/meta/simulation.py similarity index 97% rename from SimPEG/meta/simulation.py rename to simpeg/meta/simulation.py index aa62a05b0b..9fd90dcccb 100644 --- a/SimPEG/meta/simulation.py +++ b/simpeg/meta/simulation.py @@ -39,9 +39,9 @@ class MetaSimulation(BaseSimulation): Create a list of 1D simulations that perform a piece of a stitched problem. - >>> from SimPEG.simulation import ExponentialSinusoidSimulation - >>> from SimPEG import maps - >>> from SimPEG.meta import MetaSimulation + >>> from simpeg.simulation import ExponentialSinusoidSimulation + >>> from simpeg import maps + >>> from simpeg.meta import MetaSimulation >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt @@ -113,7 +113,7 @@ def simulations(self): Returns ------- - (n_sim) list of SimPEG.simulation.BaseSimulation + (n_sim) list of simpeg.simulation.BaseSimulation """ return self._simulations @@ -132,7 +132,7 @@ def mappings(self): Returns ------- - (n_sim) list of SimPEG.maps.IdentityMap + (n_sim) list of simpeg.maps.IdentityMap """ return self._mappings @@ -340,8 +340,8 @@ class SumMetaSimulation(MetaSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation - mappings : (n_sim) list of SimPEG.maps.IdentityMap + simulations : (n_sim) list of simpeg.simulation.BaseSimulation + mappings : (n_sim) list of simpeg.maps.IdentityMap """ _repeat_sim = False @@ -434,7 +434,7 @@ class RepeatedSimulation(MetaSimulation): ---------- simulation : SimPEG.simulation.BaseSimulation The simulation to use repeatedly with different mappings. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The list of different mappings to use. """ diff --git a/SimPEG/models.py b/simpeg/models.py similarity index 98% rename from SimPEG/models.py rename to simpeg/models.py index 741423a035..acf92068b6 100644 --- a/SimPEG/models.py +++ b/simpeg/models.py @@ -4,7 +4,7 @@ class Model(np.ndarray): def __new__(cls, input_array, mapping=None): - assert isinstance(mapping, IdentityMap), "mapping must be a SimPEG.Mapping" + assert isinstance(mapping, IdentityMap), "mapping must be a simpeg.Mapping" assert isinstance(input_array, np.ndarray), "input_array must be a numpy array" assert len(input_array.shape) == 1, "input_array must be a 1D vector" obj = np.asarray(input_array).view(cls) diff --git a/SimPEG/objective_function.py b/simpeg/objective_function.py similarity index 97% rename from SimPEG/objective_function.py rename to simpeg/objective_function.py index b3c299cea2..1e864d1b6e 100644 --- a/SimPEG/objective_function.py +++ b/simpeg/objective_function.py @@ -28,21 +28,21 @@ class BaseObjectiveFunction(BaseSimPEG): .. important:: If building a regularization function within SimPEG, please inherit - :py:class:`SimPEG.regularization.BaseRegularization`, as this class + :py:class:`simpeg.regularization.BaseRegularization`, as this class has additional functionality related to regularization. And if building a data misfit - function, please inherit :py:class:`SimPEG.data_misfit.BaseDataMisfit`. + function, please inherit :py:class:`simpeg.data_misfit.BaseDataMisfit`. Parameters ---------- nP : int Number of model parameters. - mapping : SimPEG.mapping.BaseMap + mapping : simpeg.mapping.BaseMap A SimPEG mapping object that maps from the model space to the quantity evaluated in the objective function. has_fields : bool If ``True``, predicted fields for a simulation and a given model can be used to evaluate the objective function quickly. - counter : None or SimPEG.utils.Counter + counter : None or simpeg.utils.Counter Assign a SimPEG ``Counter`` object to store iterations and run-times. debug : bool Print debugging information. @@ -74,7 +74,7 @@ def __call__(self, x, f=None): ---------- x : (nP) numpy.ndarray A vector representing a set of model parameters. - f : SimPEG.fields.Fields, optional + f : simpeg.fields.Fields, optional Field object (if applicable). """ @@ -113,7 +113,7 @@ def mapping(self): Returns ------- - SimPEG.mapping.BaseMap + simpeg.mapping.BaseMap The mapping from the model to the quantity evaluated in the object function. """ if self._mapping is None: @@ -301,7 +301,7 @@ class ComboObjectiveFunction(BaseObjectiveFunction): Parameters ---------- - objfcts : None or list of SimPEG.objective_function.BaseObjectiveFunction, optional + objfcts : None or list of simpeg.objective_function.BaseObjectiveFunction, optional List containing the objective functions that will live inside the composite class. If ``None``, an empty list will be created. multipliers : None or list of int, optional @@ -509,12 +509,12 @@ def get_functions_of_type(self, fun_class) -> list: Parameters ---------- - fun_class : list or SimPEG.objective_function.BaseObjectiveFunction + fun_class : list or simpeg.objective_function.BaseObjectiveFunction Objective function class or list of objective function classes to return. Returns ------- - list of SimPEG.objective_function.BaseObjectiveFunction + list of simpeg.objective_function.BaseObjectiveFunction Objective functions of a given type(s). """ target = [] @@ -545,7 +545,7 @@ class L2ObjectiveFunction(BaseObjectiveFunction): ---------- nP : int Number of model parameters. - mapping : SimPEG.mapping.BaseMap + mapping : simpeg.mapping.BaseMap A SimPEG mapping object that maps from the model space to the quantity evaluated in the objective function. W : None or scipy.sparse.csr_matrix @@ -554,7 +554,7 @@ class L2ObjectiveFunction(BaseObjectiveFunction): has_fields : bool If ``True``, predicted fields for a simulation and a given model can be used to evaluate the objective function quickly. - counter : None or SimPEG.utils.Counter + counter : None or simpeg.utils.Counter Assign a SimPEG ``Counter`` object to store iterations and run-times. debug : bool Print debugging information. diff --git a/SimPEG/optimization.py b/simpeg/optimization.py similarity index 99% rename from SimPEG/optimization.py rename to simpeg/optimization.py index dbbc23ef21..edc68b1bd5 100644 --- a/SimPEG/optimization.py +++ b/simpeg/optimization.py @@ -262,7 +262,7 @@ class Minimize(object): comment = ( "" #: Used by some functions to indicate what is going on in the algorithm ) - counter = None #: Set this to a SimPEG.utils.Counter() if you want to count things + counter = None #: Set this to a simpeg.utils.Counter() if you want to count things parent = None #: This is the parent of the optimization routine. print_type = None @@ -945,7 +945,7 @@ def bfgsH0(self): """ Approximate Hessian used in preconditioning the problem. - Must be a SimPEG.Solver + Must be a simpeg.Solver """ if getattr(self, "_bfgsH0", None) is None: print( @@ -975,7 +975,7 @@ def bfgs(self, d): def bfgsrec(self, k, n, nn, S, Y, d): """BFGS recursion""" if k < 0: - d = self.bfgsH0 * d # Assume that bfgsH0 is a SimPEG.Solver + d = self.bfgsH0 * d # Assume that bfgsH0 is a simpeg.Solver else: khat = 0 if nn == 0 else np.mod(n - nn + k, nn) gamma = np.vdot(S[:, khat], d) / np.vdot(Y[:, khat], S[:, khat]) @@ -1034,7 +1034,7 @@ class InexactGaussNewton(BFGS, Minimize, Remember): Use *nbfgs* to set the memory limitation of BFGS. To set the initial H0 to be used in BFGS, set *bfgsH0* to be a - SimPEG.Solver + simpeg.Solver """ diff --git a/SimPEG/potential_fields/__init__.py b/simpeg/potential_fields/__init__.py similarity index 81% rename from SimPEG/potential_fields/__init__.py rename to simpeg/potential_fields/__init__.py index e46138b225..21c0a35ce1 100644 --- a/SimPEG/potential_fields/__init__.py +++ b/simpeg/potential_fields/__init__.py @@ -1,8 +1,8 @@ """ ================================================================= -Base Classes and Functions (:mod:`SimPEG.potential_fields`) +Base Classes and Functions (:mod:`simpeg.potential_fields`) ================================================================= -.. currentmodule:: SimPEG.potential_fields +.. currentmodule:: simpeg.potential_fields About ``potential_fields`` diff --git a/SimPEG/potential_fields/base.py b/simpeg/potential_fields/base.py similarity index 99% rename from SimPEG/potential_fields/base.py rename to simpeg/potential_fields/base.py index 9c93895039..d457483e3e 100644 --- a/SimPEG/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -5,7 +5,7 @@ import numpy as np from scipy.sparse import csr_matrix as csr -from SimPEG.utils import mkvc +from simpeg.utils import mkvc from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string @@ -57,7 +57,7 @@ class BasePFSimulation(LinearSimulation): Therefor you must protect the calls to this class by testing if you are in the main process with: - >>> from SimPEG.potential_fields import gravity + >>> from simpeg.potential_fields import gravity >>> if __name__ == '__main__': ... # Do your processing here ... sim = gravity.Simulation3DIntegral(n_processes=4, ...) diff --git a/SimPEG/potential_fields/gravity/__init__.py b/simpeg/potential_fields/gravity/__init__.py similarity index 89% rename from SimPEG/potential_fields/gravity/__init__.py rename to simpeg/potential_fields/gravity/__init__.py index ae4e97687e..193cad2f19 100644 --- a/SimPEG/potential_fields/gravity/__init__.py +++ b/simpeg/potential_fields/gravity/__init__.py @@ -1,8 +1,8 @@ """ ======================================================================= -Gravity Simulation (:mod:`SimPEG.potential_fields.gravity`) +Gravity Simulation (:mod:`simpeg.potential_fields.gravity`) ======================================================================= -.. currentmodule:: SimPEG.potential_fields.gravity +.. currentmodule:: simpeg.potential_fields.gravity About ``gravity`` diff --git a/SimPEG/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py similarity index 100% rename from SimPEG/potential_fields/gravity/_numba_functions.py rename to simpeg/potential_fields/gravity/_numba_functions.py diff --git a/SimPEG/potential_fields/gravity/analytics.py b/simpeg/potential_fields/gravity/analytics.py similarity index 98% rename from SimPEG/potential_fields/gravity/analytics.py rename to simpeg/potential_fields/gravity/analytics.py index ca86d744ba..e8da42db79 100644 --- a/SimPEG/potential_fields/gravity/analytics.py +++ b/simpeg/potential_fields/gravity/analytics.py @@ -1,5 +1,5 @@ from scipy.constants import G -from SimPEG.utils import mkvc +from simpeg.utils import mkvc import numpy as np diff --git a/SimPEG/potential_fields/gravity/receivers.py b/simpeg/potential_fields/gravity/receivers.py similarity index 97% rename from SimPEG/potential_fields/gravity/receivers.py rename to simpeg/potential_fields/gravity/receivers.py index 48fe680205..de54848cf5 100644 --- a/SimPEG/potential_fields/gravity/receivers.py +++ b/simpeg/potential_fields/gravity/receivers.py @@ -45,7 +45,7 @@ class Point(survey.BaseRx): See also -------- - SimPEG.potential_fields.gravity.Simulation3DIntegral + simpeg.potential_fields.gravity.Simulation3DIntegral """ def __init__(self, locations, components="gz", **kwargs): diff --git a/SimPEG/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py similarity index 99% rename from SimPEG/potential_fields/gravity/simulation.py rename to simpeg/potential_fields/gravity/simulation.py index a520019939..fdc2b68d82 100644 --- a/SimPEG/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -6,8 +6,8 @@ from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG -from SimPEG import props -from SimPEG.utils import mkvc, sdiag +from simpeg import props +from simpeg.utils import mkvc, sdiag from ...base import BasePDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation diff --git a/SimPEG/potential_fields/gravity/sources.py b/simpeg/potential_fields/gravity/sources.py similarity index 83% rename from SimPEG/potential_fields/gravity/sources.py rename to simpeg/potential_fields/gravity/sources.py index 93c1015cd1..3e890dc68a 100644 --- a/SimPEG/potential_fields/gravity/sources.py +++ b/simpeg/potential_fields/gravity/sources.py @@ -6,7 +6,7 @@ class SourceField(BaseSrc): Parameters ---------- - receivers_list : list of SimPEG.potential_fields.receivers.Point + receivers_list : list of simpeg.potential_fields.receivers.Point List of magnetics receivers """ diff --git a/SimPEG/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py similarity index 100% rename from SimPEG/potential_fields/gravity/survey.py rename to simpeg/potential_fields/gravity/survey.py diff --git a/SimPEG/potential_fields/magnetics/__init__.py b/simpeg/potential_fields/magnetics/__init__.py similarity index 89% rename from SimPEG/potential_fields/magnetics/__init__.py rename to simpeg/potential_fields/magnetics/__init__.py index 6d9241e310..52612898b8 100644 --- a/SimPEG/potential_fields/magnetics/__init__.py +++ b/simpeg/potential_fields/magnetics/__init__.py @@ -1,8 +1,8 @@ """ ========================================================================= -Magnetics Simulation (:mod:`SimPEG.potential_fields.magnetics`) +Magnetics Simulation (:mod:`simpeg.potential_fields.magnetics`) ========================================================================= -.. currentmodule:: SimPEG.potential_fields.magnetics +.. currentmodule:: simpeg.potential_fields.magnetics About ``magnetics`` diff --git a/SimPEG/potential_fields/magnetics/analytics.py b/simpeg/potential_fields/magnetics/analytics.py similarity index 99% rename from SimPEG/potential_fields/magnetics/analytics.py rename to simpeg/potential_fields/magnetics/analytics.py index a9bdd7ad1f..fc6f7afba2 100644 --- a/SimPEG/potential_fields/magnetics/analytics.py +++ b/simpeg/potential_fields/magnetics/analytics.py @@ -1,7 +1,7 @@ from scipy.constants import mu_0 -from SimPEG import utils +from simpeg import utils -# from SimPEG import Mesh +# from simpeg import Mesh import numpy as np diff --git a/SimPEG/potential_fields/magnetics/receivers.py b/simpeg/potential_fields/magnetics/receivers.py similarity index 100% rename from SimPEG/potential_fields/magnetics/receivers.py rename to simpeg/potential_fields/magnetics/receivers.py diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py similarity index 99% rename from SimPEG/potential_fields/magnetics/simulation.py rename to simpeg/potential_fields/magnetics/simulation.py index f90145000c..5d3d27171c 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -11,9 +11,9 @@ ) from scipy.constants import mu_0 -from SimPEG import Solver, props, utils -from SimPEG.utils import mat_utils, mkvc, sdiag -from SimPEG.utils.code_utils import deprecate_property, validate_string, validate_type +from simpeg import Solver, props, utils +from simpeg.utils import mat_utils, mkvc, sdiag +from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation @@ -535,7 +535,7 @@ def survey(self): Returns ------- - SimPEG.potential_fields.magnetics.survey.Survey + simpeg.potential_fields.magnetics.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") @@ -996,7 +996,7 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): Inversion module for MagneticsDiffSecondary """ - from SimPEG import ( + from simpeg import ( directives, inversion, objective_function, diff --git a/SimPEG/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py similarity index 93% rename from SimPEG/potential_fields/magnetics/sources.py rename to simpeg/potential_fields/magnetics/sources.py index 33c9137ea3..0e4000e9c2 100644 --- a/SimPEG/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,6 @@ from ...survey import BaseSrc -from SimPEG.utils.mat_utils import dip_azimuth2cartesian -from SimPEG.utils.code_utils import deprecate_class, validate_float +from simpeg.utils.mat_utils import dip_azimuth2cartesian +from simpeg.utils.code_utils import deprecate_class, validate_float class UniformBackgroundField(BaseSrc): @@ -11,7 +11,7 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.potential_fields.magnetics.Point + receiver_list : list of simpeg.potential_fields.magnetics.Point amplitude : float, optional amplitude of the inducing backgound field, usually this is in units of nT. inclination : float, optional @@ -106,7 +106,7 @@ class SourceField(UniformBackgroundField): Parameters ---------- - receivers_list : list of SimPEG.potential_fields.receivers.Point + receivers_list : list of simpeg.potential_fields.receivers.Point List of magnetics receivers parameters : (3) array_like of float Define the Earth's inducing field according to diff --git a/SimPEG/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py similarity index 95% rename from SimPEG/potential_fields/magnetics/survey.py rename to simpeg/potential_fields/magnetics/survey.py index 98ac827a5c..56a2c3296c 100644 --- a/SimPEG/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -9,7 +9,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.magnetics.sources.UniformBackgroundField + source_field : simpeg.potential_fields.magnetics.sources.UniformBackgroundField A source object that defines the Earth's inducing field """ @@ -100,4 +100,4 @@ def vnD(self): # make this look like it lives in the below module -Survey.__module__ = "SimPEG.potential_fields.magnetics" +Survey.__module__ = "simpeg.potential_fields.magnetics" diff --git a/SimPEG/props.py b/simpeg/props.py similarity index 99% rename from SimPEG/props.py rename to simpeg/props.py index 8732b012d4..5a55f77b0f 100644 --- a/SimPEG/props.py +++ b/simpeg/props.py @@ -38,7 +38,7 @@ def get_property(scope): Returns ------- - SimPEG.maps.IdentityMap + simpeg.maps.IdentityMap """ def fget(self): diff --git a/SimPEG/regularization/__init__.py b/simpeg/regularization/__init__.py similarity index 99% rename from SimPEG/regularization/__init__.py rename to simpeg/regularization/__init__.py index 9026acb078..dbc30a5984 100644 --- a/SimPEG/regularization/__init__.py +++ b/simpeg/regularization/__init__.py @@ -1,9 +1,9 @@ r""" ============================================= -Regularization (:mod:`SimPEG.regularization`) +Regularization (:mod:`simpeg.regularization`) ============================================= -.. currentmodule:: SimPEG.regularization +.. currentmodule:: simpeg.regularization ``Regularization`` classes are used to impose constraints on models recovered through geophysical inversion. Constraints may be straight forward, such as: requiring the recovered model be diff --git a/SimPEG/regularization/_gradient.py b/simpeg/regularization/_gradient.py similarity index 99% rename from SimPEG/regularization/_gradient.py rename to simpeg/regularization/_gradient.py index 7e98309f48..570e4727aa 100644 --- a/SimPEG/regularization/_gradient.py +++ b/simpeg/regularization/_gradient.py @@ -35,7 +35,7 @@ class SmoothnessFullGradient(BaseRegularization): Construct of 2D measure with uniform smoothing in each direction. >>> from discretize import TensorMesh - >>> from SimPEG.regularization import SmoothnessFullGradient + >>> from simpeg.regularization import SmoothnessFullGradient >>> mesh = TensorMesh([32, 32]) >>> reg = SmoothnessFullGradient(mesh) diff --git a/SimPEG/regularization/base.py b/simpeg/regularization/base.py similarity index 98% rename from SimPEG/regularization/base.py rename to simpeg/regularization/base.py index 935efa9bba..823c7fd0de 100644 --- a/SimPEG/regularization/base.py +++ b/simpeg/regularization/base.py @@ -8,7 +8,7 @@ from .. import utils from .regularization_mesh import RegularizationMesh -from SimPEG.utils.code_utils import deprecate_property, validate_ndarray_with_shape +from simpeg.utils.code_utils import deprecate_property, validate_ndarray_with_shape if TYPE_CHECKING: from scipy.sparse import csr_matrix @@ -22,15 +22,15 @@ class BaseRegularization(BaseObjectiveFunction): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is set to :obj:`SimPEG.maps.IdentityMap`. + If ``None``, the mapping is set to :obj:`simpeg.maps.IdentityMap`. reference_model : None, (n_param, ) numpy.ndarray Reference model. If ``None``, the reference model in the inversion is set to the starting model. @@ -161,7 +161,7 @@ def mapping(self) -> maps.IdentityMap: Returns ------- - SimPEG.maps.BaseMap + simpeg.maps.BaseMap The mapping from the inversion model parameters to the quantity defined on the :py:class:`~.regularization.RegularizationMesh`. """ @@ -323,7 +323,7 @@ def get_weights(self, key) -> np.ndarray: Examples -------- >>> import discretize - >>> from SimPEG.regularization import Smallness + >>> from simpeg.regularization import Smallness >>> mesh = discretize.TensorMesh([2, 3, 2]) >>> reg = Smallness(mesh) >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) @@ -345,7 +345,7 @@ def set_weights(self, **weights): Examples -------- >>> import discretize - >>> from SimPEG.regularization import Smallness + >>> from simpeg.regularization import Smallness >>> mesh = discretize.TensorMesh([2, 3, 2]) >>> reg = Smallness(mesh) >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) @@ -378,7 +378,7 @@ def remove_weights(self, key): Examples -------- >>> import discretize - >>> from SimPEG.regularization import Smallness + >>> from simpeg.regularization import Smallness >>> mesh = discretize.TensorMesh([2, 3, 2]) >>> reg = Smallness(mesh) >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) @@ -542,7 +542,7 @@ class Smallness(BaseRegularization): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -745,7 +745,7 @@ class SmoothnessFirstOrder(BaseRegularization): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -1113,7 +1113,7 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -1369,13 +1369,13 @@ class WeightedLeastSquares(ComboObjectiveFunction): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -1676,7 +1676,7 @@ def set_weights(self, **weights): Examples -------- >>> import discretize - >>> from SimPEG.regularization import WeightedLeastSquares + >>> from simpeg.regularization import WeightedLeastSquares >>> mesh = discretize.TensorMesh([2, 3, 2]) >>> reg = WeightedLeastSquares(mesh) >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) @@ -1698,7 +1698,7 @@ def remove_weights(self, key): Examples -------- >>> import discretize - >>> from SimPEG.regularization import WeightedLeastSquares + >>> from simpeg.regularization import WeightedLeastSquares >>> mesh = discretize.TensorMesh([2, 3, 2]) >>> reg = WeightedLeastSquares(mesh) >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) @@ -2185,9 +2185,9 @@ def mapping(self) -> maps.IdentityMap: Returns ------- - SimPEG.maps.BaseMap + simpeg.maps.BaseMap The mapping from the model parameters to the quantity defined on the - :py:class:`~SimPEG.regularization.RegularizationMesh`. + :py:class:`~simpeg.regularization.RegularizationMesh`. """ return self._mapping @@ -2221,10 +2221,10 @@ class BaseSimilarityMeasure(BaseRegularization): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh + mesh : simpeg.regularization.RegularizationMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - wire_map : SimPEG.maps.WireMap + wire_map : simpeg.maps.WireMap Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh` to the entire model. """ @@ -2239,7 +2239,7 @@ def wire_map(self): Returns ------- - SimPEG.maps.WireMap + simpeg.maps.WireMap Mapping from model to physical properties defined on the regularization mesh. """ return self._wire_map diff --git a/SimPEG/regularization/correspondence.py b/simpeg/regularization/correspondence.py similarity index 99% rename from SimPEG/regularization/correspondence.py rename to simpeg/regularization/correspondence.py index 670afc3132..fe9060dc5b 100644 --- a/SimPEG/regularization/correspondence.py +++ b/simpeg/regularization/correspondence.py @@ -16,7 +16,7 @@ class LinearCorrespondence(BaseSimilarityMeasure): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool diff --git a/SimPEG/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py similarity index 99% rename from SimPEG/regularization/cross_gradient.py rename to simpeg/regularization/cross_gradient.py index a1cb7e1c03..e245233a1a 100644 --- a/SimPEG/regularization/cross_gradient.py +++ b/simpeg/regularization/cross_gradient.py @@ -23,7 +23,7 @@ class CrossGradient(BaseSimilarityMeasure): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool diff --git a/SimPEG/regularization/jtv.py b/simpeg/regularization/jtv.py similarity index 99% rename from SimPEG/regularization/jtv.py rename to simpeg/regularization/jtv.py index 86a2208915..ce37a309db 100644 --- a/SimPEG/regularization/jtv.py +++ b/simpeg/regularization/jtv.py @@ -21,7 +21,7 @@ class JointTotalVariation(BaseSimilarityMeasure): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool diff --git a/SimPEG/regularization/pgi.py b/simpeg/regularization/pgi.py similarity index 99% rename from SimPEG/regularization/pgi.py rename to simpeg/regularization/pgi.py index 24aeaf866d..24be874eff 100644 --- a/SimPEG/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -53,21 +53,21 @@ class PGIsmallness(Smallness): ---------- gmmref : SimPEG.utils.WeightedGaussianMixture Reference Gaussian mixture model. - gmm : None, SimPEG.utils.WeightedGaussianMixture + gmm : None, simpeg.utils.WeightedGaussianMixture Set the Gaussian mixture model used to constrain the recovered physical property model. Can be left static throughout the inversion or updated using the :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there is a Gaussian mixture model for the inversion. - wiresmap : None, SimPEG.maps.Wires + wiresmap : None, simpeg.maps.Wires Mapping from the model to the model parameters of each type. If ``None``, we assume only a single physical property type in the inversion. - maplist : None, list of SimPEG.maps + maplist : None, list of simpeg.maps Ordered list of mappings from model values to physical property values; one for each physical property. If ``None``, we assume a single physical property type in the regularization and an :class:`.maps.IdentityMap` from model values to physical property values. - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. Implemented for ``tensor``, ``QuadTree`` or ``Octree`` meshes. approx_gradient : bool @@ -398,7 +398,7 @@ def maplist(self): Returns ------- - list of SimPEG.maps + list of simpeg.maps Ordered list of mappings from model values to physical property values; one for each physical property. """ @@ -972,12 +972,12 @@ class PGI(ComboObjectiveFunction): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. Implemented for `tensor`, `QuadTree` or `Octree` meshes. gmmref : SimPEG.utils.WeightedGaussianMixture Reference Gaussian mixture model. - gmm : None, SimPEG.utils.WeightedGaussianMixture + gmm : None, simpeg.utils.WeightedGaussianMixture Set the Gaussian mixture model used to constrain the recovered physical property model. Can be left static throughout the inversion or updated using the :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the @@ -993,10 +993,10 @@ class PGI(ComboObjectiveFunction): Scaling constants for the second order smoothness along x, y and z, respectively. If set to ``None``, the scaling constant is set automatically according to the length scales; see :class:`regularization.WeightedLeastSquares`. - wiresmap : None, SimPEG.maps.Wires + wiresmap : None, simpeg.maps.Wires Mapping from the model to the model parameters of each type. If ``None``, we assume only a single physical property type in the inversion. - maplist : None, list of SimPEG.maps + maplist : None, list of simpeg.maps Ordered list of mappings from model values to physical property values; one for each physical property. If ``None``, we assume a single physical property type in the regularization and an :class:`.maps.IdentityMap` from model values to physical @@ -1236,7 +1236,7 @@ def gmm(self): Returns ------- - None, SimPEG.utils.WeightedGaussianMixture + None, simpeg.utils.WeightedGaussianMixture Gaussian mixture model. """ return self.objfcts[0].gmm @@ -1314,7 +1314,7 @@ def maplist(self): Returns ------- - list of SimPEG.maps + list of simpeg.maps Ordered list of mappings from model values to physical property values; one for each physical property. """ diff --git a/SimPEG/regularization/regularization_mesh.py b/simpeg/regularization/regularization_mesh.py similarity index 99% rename from SimPEG/regularization/regularization_mesh.py rename to simpeg/regularization/regularization_mesh.py index 63eaa981f2..dea11bb2f1 100755 --- a/SimPEG/regularization/regularization_mesh.py +++ b/simpeg/regularization/regularization_mesh.py @@ -1,7 +1,7 @@ import numpy as np import scipy.sparse as sp -from SimPEG.utils.code_utils import deprecate_property, validate_active_indices +from simpeg.utils.code_utils import deprecate_property, validate_active_indices from .. import props, utils @@ -590,4 +590,4 @@ def cell_distances_z(self) -> np.ndarray: # Make it look like it's in the regularization module -RegularizationMesh.__module__ = "SimPEG.regularization" +RegularizationMesh.__module__ = "simpeg.regularization" diff --git a/SimPEG/regularization/sparse.py b/simpeg/regularization/sparse.py similarity index 99% rename from SimPEG/regularization/sparse.py rename to simpeg/regularization/sparse.py index 43d5916a03..a917e7ecbd 100644 --- a/SimPEG/regularization/sparse.py +++ b/simpeg/regularization/sparse.py @@ -30,13 +30,13 @@ class BaseSparse(BaseRegularization): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -233,7 +233,7 @@ class SparseSmallness(BaseSparse, Smallness): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -430,7 +430,7 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -717,13 +717,13 @@ class Sparse(WeightedLeastSquares): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray diff --git a/SimPEG/regularization/vector.py b/simpeg/regularization/vector.py similarity index 99% rename from SimPEG/regularization/vector.py rename to simpeg/regularization/vector.py index 19bd68f080..cc2f628c44 100644 --- a/SimPEG/regularization/vector.py +++ b/simpeg/regularization/vector.py @@ -64,7 +64,7 @@ class CrossReferenceRegularization(Smallness, BaseVectorRegularization): active_cells : index_array, optional Boolean array or an array of active indices indicating the active cells of the inversion domain mesh. - mapping : SimPEG.maps.IdentityMap, optional + mapping : simpeg.maps.IdentityMap, optional An optional linear mapping that would go from the model space to the space where the cross-product is enforced. weights : dict of [str: array_like], optional @@ -506,7 +506,7 @@ class AmplitudeSmallness(SparseSmallness, BaseAmplitude): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -716,7 +716,7 @@ class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray @@ -1004,13 +1004,13 @@ class VectorAmplitude(Sparse): Parameters ---------- - mesh : SimPEG.regularization.RegularizationMesh, discretize.base.BaseMesh + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. This is not necessarily the same as the mesh on which the simulation is defined. active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, SimPEG.maps.BaseMap + mapping : None, simpeg.maps.BaseMap The mapping from the model parameters to the active cells in the inversion. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray diff --git a/SimPEG/seismic/__init__.py b/simpeg/seismic/__init__.py similarity index 100% rename from SimPEG/seismic/__init__.py rename to simpeg/seismic/__init__.py diff --git a/SimPEG/seismic/straight_ray_tomography/__init__.py b/simpeg/seismic/straight_ray_tomography/__init__.py similarity index 83% rename from SimPEG/seismic/straight_ray_tomography/__init__.py rename to simpeg/seismic/straight_ray_tomography/__init__.py index 69e8a06036..c447198b10 100644 --- a/SimPEG/seismic/straight_ray_tomography/__init__.py +++ b/simpeg/seismic/straight_ray_tomography/__init__.py @@ -1,8 +1,8 @@ """ ================================================================================== -Straight Ray Tomography (:mod:`SimPEG.seismic.straight_ray_tomography`) +Straight Ray Tomography (:mod:`simpeg.seismic.straight_ray_tomography`) ================================================================================== -.. currentmodule:: SimPEG.seismic.straight_ray_tomography +.. currentmodule:: simpeg.seismic.straight_ray_tomography About ``straight_ray_tomography`` diff --git a/SimPEG/seismic/straight_ray_tomography/simulation.py b/simpeg/seismic/straight_ray_tomography/simulation.py similarity index 100% rename from SimPEG/seismic/straight_ray_tomography/simulation.py rename to simpeg/seismic/straight_ray_tomography/simulation.py diff --git a/SimPEG/seismic/straight_ray_tomography/survey.py b/simpeg/seismic/straight_ray_tomography/survey.py similarity index 100% rename from SimPEG/seismic/straight_ray_tomography/survey.py rename to simpeg/seismic/straight_ray_tomography/survey.py diff --git a/SimPEG/simulation.py b/simpeg/simulation.py similarity index 98% rename from SimPEG/simulation.py rename to simpeg/simulation.py index f6cd144b1a..cc751647cd 100644 --- a/SimPEG/simulation.py +++ b/simpeg/simulation.py @@ -55,7 +55,7 @@ class BaseSimulation(props.HasModel): ---------- mesh : discretize.base.BaseMesh, optional Mesh on which the forward problem is discretized. - survey : SimPEG.survey.BaseSurvey, optional + survey : simpeg.survey.BaseSurvey, optional The survey for the simulation. solver : None or pymatsolver.base.Base, optional Numerical solver used to solve the forward problem. If ``None``, @@ -68,7 +68,7 @@ class BaseSimulation(props.HasModel): about solvers and their parameters. sensitivity_path : str, optional Path to directory where sensitivity file is stored. - counter : None or SimPEG.utils.Counter + counter : None or simpeg.utils.Counter SimPEG ``Counter`` object to store iterations and run-times. verbose : bool, optional Verbose progress printout. @@ -128,7 +128,7 @@ def survey(self): Returns ------- - SimPEG.survey.BaseSurvey + simpeg.survey.BaseSurvey The survey for the simulation. """ return self._survey @@ -145,7 +145,7 @@ def counter(self): Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter SimPEG ``Counter`` object to store iterations and run-times. """ return self._counter @@ -250,7 +250,7 @@ def fields(self, m=None): Returns ------- - SimPEG.fields.Fields + simpeg.fields.Fields Computed geophysical fields for the model provided. """ @@ -263,7 +263,7 @@ def dpred(self, m=None, f=None): ---------- m : (n_param,) numpy.ndarray The model parameters. - f : SimPEG.fields.Fields, optional + f : simpeg.fields.Fields, optional If provided, will be used to compute the predicted data without recalculating the fields. @@ -314,7 +314,7 @@ def Jvec(self, m, v, f=None): The model parameters. v : (n_param, ) numpy.ndarray Vector we are multiplying. - f : SimPEG.field.Fields, optional + f : simpeg.field.Fields, optional If provided, fields will not need to be recomputed for the current model to compute `Jvec`. @@ -348,7 +348,7 @@ def Jtvec(self, m, v, f=None): The model parameters. v : (n_data, ) numpy.ndarray Vector we are multiplying. - f : SimPEG.field.Fields, optional + f : simpeg.field.Fields, optional If provided, fields will not need to be recomputed for the current model to compute `Jtvec`. @@ -383,7 +383,7 @@ def Jvec_approx(self, m, v, f=None): The model parameters. v : (n_data, ) numpy.ndarray Vector we are multiplying. - f : SimPEG.field.Fields, optional + f : simpeg.field.Fields, optional If provided, fields will not need to be recomputed for the current model to compute `Jtvec`. @@ -418,7 +418,7 @@ def Jtvec_approx(self, m, v, f=None): The model parameters. v : (n_data, ) numpy.ndarray Vector we are multiplying. - f : SimPEG.field.Fields, optional + f : simpeg.field.Fields, optional If provided, fields will not need to be recomputed for the current model to compute `Jtvec`. @@ -447,7 +447,7 @@ def residual(self, m, dobs, f=None): The model parameters. dobs : (n_data, ) numpy.ndarray The observed data values. - f : SimPEG.fields.Fields, optional + f : simpeg.fields.Fields, optional If provided, fields will not need to be recomputed when solving the forward problem. Returns @@ -470,7 +470,7 @@ def make_synthetic_data( ): r"""Make synthetic data for the model and Gaussian noise provided. - This method generates and returns a :py:class:`SimPEG.data.SyntheticData` object + This method generates and returns a :py:class:`simpeg.data.SyntheticData` object for the model and standard deviation of Gaussian noise provided. Parameters @@ -485,7 +485,7 @@ def make_synthetic_data( noise_floor : float, numpy.ndarray Assign floor/absolute uncertainties to the data. For each datum, we assume standard deviation of Gaussian noise is equal to `noise_floor`. - f : SimPEG.fields.Fields, optional + f : simpeg.fields.Fields, optional If provided, fields will not need to be recomputed when solving the forward problem to obtain noiseless data. add_noise : bool @@ -495,7 +495,7 @@ def make_synthetic_data( Returns ------- - SimPEG.data.SyntheticData + simpeg.data.SyntheticData A SimPEG synthetic data object, which organizes both clean and noisy data. """ @@ -738,7 +738,7 @@ class LinearSimulation(BaseSimulation): mesh : discretize.BaseMesh, optional Mesh on which the forward problem is discretized. This is not necessarily the same as the mesh on which the simulation is defined. - model_map : SimPEG.maps.BaseMap + model_map : simpeg.maps.BaseMap Mapping from the model parameters to vector that the linear operator acts on. G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx The linear operator. For a ``model_map`` that maps within the same vector space diff --git a/SimPEG/survey.py b/simpeg/survey.py similarity index 98% rename from SimPEG/survey.py rename to simpeg/survey.py index dad847cff4..57b534ea6a 100644 --- a/SimPEG/survey.py +++ b/simpeg/survey.py @@ -284,7 +284,7 @@ class BaseSrc: Parameters ---------- - receiver_list : list of SimPEG.survey.BaseRx objects + receiver_list : list of simpeg.survey.BaseRx objects Sets the receivers associated with the source location : (n_dim) numpy.ndarray Location of the source @@ -327,7 +327,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.survey.BaseRx + list of simpeg.survey.BaseRx List of receivers associated with the source """ return self._receiver_list @@ -366,7 +366,7 @@ def get_receiver_indices(self, receivers): Parameters ---------- - receivers : list of SimPEG.survey.BaseRx + receivers : list of simpeg.survey.BaseRx A subset list of receivers within the source's receivers list Returns @@ -418,9 +418,9 @@ class BaseSurvey: Parameters ---------- - source_list : list of SimPEG.survey.BaseSrc objects + source_list : list of simpeg.survey.BaseSrc objects Sets the sources (and their receivers) - counter : SimPEG.utils.Counter + counter : simpeg.utils.Counter A SimPEG counter object """ @@ -442,7 +442,7 @@ def source_list(self): Returns ------- - list of SimPEG.survey.BaseSrc + list of simpeg.survey.BaseSrc List of sources associated with the survey """ return self._source_list @@ -487,7 +487,7 @@ def counter(self): Returns ------- - SimPEG.utils.counter_utils.Counter + simpeg.utils.counter_utils.Counter A SimPEG counter object """ return self._counter diff --git a/SimPEG/utils/__init__.py b/simpeg/utils/__init__.py similarity index 98% rename from SimPEG/utils/__init__.py rename to simpeg/utils/__init__.py index b023970eca..d5506d510c 100644 --- a/SimPEG/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Utility Classes and Functions (:mod:`SimPEG.utils`) +Utility Classes and Functions (:mod:`simpeg.utils`) ======================================================== -.. currentmodule:: SimPEG.utils +.. currentmodule:: simpeg.utils The ``utils`` package contains utilities for helping with common operations involving SimPEG. diff --git a/SimPEG/utils/code_utils.py b/simpeg/utils/code_utils.py similarity index 99% rename from SimPEG/utils/code_utils.py rename to simpeg/utils/code_utils.py index 58e759118d..8c2014b216 100644 --- a/SimPEG/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -6,7 +6,7 @@ from discretize.utils import as_array_n_by_dim # noqa: F401 -# scooby is a soft dependency for SimPEG +# scooby is a soft dependency for simpeg try: from scooby import Report as ScoobyReport except ImportError: @@ -14,7 +14,7 @@ class ScoobyReport: def __init__(self, additional, core, optional, ncol, text_width, sort): print( - "\n *ERROR*: `SimPEG.Report` requires `scooby`." + "\n *ERROR*: `simpeg.Report` requires `scooby`." "\n Install it via `pip install scooby` or" "\n `conda install -c conda-forge scooby`.\n" ) @@ -454,7 +454,7 @@ class Report(ScoobyReport): >>> import pytest >>> import dateutil - >>> from SimPEG import Report + >>> from simpeg import Report >>> Report() # Default values >>> Report(pytest) # Provide additional package >>> Report([pytest, dateutil], ncol=5) # Define nr of columns @@ -466,7 +466,7 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): # Mandatory packages. core = [ - "SimPEG", + "simpeg", "discretize", "pymatsolver", "numpy", diff --git a/SimPEG/utils/coord_utils.py b/simpeg/utils/coord_utils.py similarity index 100% rename from SimPEG/utils/coord_utils.py rename to simpeg/utils/coord_utils.py diff --git a/SimPEG/utils/counter_utils.py b/simpeg/utils/counter_utils.py similarity index 98% rename from SimPEG/utils/counter_utils.py rename to simpeg/utils/counter_utils.py index c570de0a65..ded65c2cd8 100644 --- a/SimPEG/utils/counter_utils.py +++ b/simpeg/utils/counter_utils.py @@ -16,7 +16,7 @@ class Counter(object): decorators on class methods. - >>> from SimPEG.utils import Counter, count, timeIt + >>> from simpeg.utils import Counter, count, timeIt >>> class MyClass(object): ... def __init__(self, url): diff --git a/SimPEG/utils/curv_utils.py b/simpeg/utils/curv_utils.py similarity index 100% rename from SimPEG/utils/curv_utils.py rename to simpeg/utils/curv_utils.py diff --git a/SimPEG/utils/drivers/__init__.py b/simpeg/utils/drivers/__init__.py similarity index 100% rename from SimPEG/utils/drivers/__init__.py rename to simpeg/utils/drivers/__init__.py diff --git a/SimPEG/utils/drivers/gravity_driver.py b/simpeg/utils/drivers/gravity_driver.py similarity index 99% rename from SimPEG/utils/drivers/gravity_driver.py rename to simpeg/utils/drivers/gravity_driver.py index 2d25574212..77c6fcfde4 100644 --- a/SimPEG/utils/drivers/gravity_driver.py +++ b/simpeg/utils/drivers/gravity_driver.py @@ -3,7 +3,7 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import utils +from simpeg import utils import numpy as np diff --git a/SimPEG/utils/drivers/magnetics_driver.py b/simpeg/utils/drivers/magnetics_driver.py similarity index 99% rename from SimPEG/utils/drivers/magnetics_driver.py rename to simpeg/utils/drivers/magnetics_driver.py index a6f2455193..60b799afe8 100644 --- a/SimPEG/utils/drivers/magnetics_driver.py +++ b/simpeg/utils/drivers/magnetics_driver.py @@ -3,7 +3,7 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import utils +from simpeg import utils import numpy as np @@ -307,7 +307,7 @@ def magnetizationModel(self): # Convert list to 2d array M = np.vstack(M) - # Cycle through three components and permute from UBC to SimPEG + # Cycle through three components and permute from UBC to simpeg for ii in range(3): m = np.reshape( M[:, ii], diff --git a/SimPEG/utils/io_utils/__init__.py b/simpeg/utils/io_utils/__init__.py similarity index 100% rename from SimPEG/utils/io_utils/__init__.py rename to simpeg/utils/io_utils/__init__.py diff --git a/SimPEG/utils/io_utils/io_utils_electromagnetics.py b/simpeg/utils/io_utils/io_utils_electromagnetics.py similarity index 96% rename from SimPEG/utils/io_utils/io_utils_electromagnetics.py rename to simpeg/utils/io_utils/io_utils_electromagnetics.py index 552f72fbb6..6d44676987 100644 --- a/SimPEG/utils/io_utils/io_utils_electromagnetics.py +++ b/simpeg/utils/io_utils/io_utils_electromagnetics.py @@ -28,7 +28,7 @@ def read_dcip_xyz( locations provided. This function is versatile enough to load 2D or 3D data. The data file may include elevations for the electrodes or be surface formatted. Columns containing data which are not defined as part of a - :class:`SimPEG.data.Data` object may be loaded and output to a dictionary. + :class:`simpeg.data.Data` object may be loaded and output to a dictionary. Parameters ---------- @@ -62,10 +62,10 @@ def read_dcip_xyz( Returns ------- - SimPEG.data.Data + simpeg.data.Data DC or IP data. The survey attribute associated with the data object will be an - instance of :class:`SimPEG.electromagnetics.static.resistivity.survey.Survey` - or :class:`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + instance of :class:`simpeg.electromagnetics.static.resistivity.survey.Survey` + or :class:`simpeg.electromagnetics.static.induced_polarization.survey.Survey` dict If additional columns are loaded and output to a dictionary using the keyward argument `dict_headers`, the output of this function has the form `(out_data, out_dict)`. @@ -211,7 +211,7 @@ def read_dcip2d_ubc(file_name, data_type, format_type): A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -439,7 +439,7 @@ def read_dcip3d_ubc(file_name, data_type): A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resitivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resitivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -599,7 +599,7 @@ def read_dcipoctree_ubc(file_name, data_type): A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -627,9 +627,9 @@ def write_dcip2d_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} The type of data. file_type : {'survey', 'dpred', 'dobs'} @@ -803,9 +803,9 @@ def write_dcip3d_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} file_type : {'survey', 'dpred', 'dobs'} format_type : {'general', 'surface'} @@ -958,9 +958,9 @@ def write_dcipoctree_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} file_type : {'survey', 'dpred', 'dobs'} format_type : {'general', 'surface'} @@ -993,9 +993,9 @@ def write_dcip_xyz( file_name : str Path to the file data_object : SimPEG.data.Data - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_header: str Header for the data column; i.e. the header for the data defined in the `dobs` attibute of the data object. If ``None``, these data are not written to file diff --git a/SimPEG/utils/io_utils/io_utils_general.py b/simpeg/utils/io_utils/io_utils_general.py similarity index 100% rename from SimPEG/utils/io_utils/io_utils_general.py rename to simpeg/utils/io_utils/io_utils_general.py diff --git a/SimPEG/utils/io_utils/io_utils_pf.py b/simpeg/utils/io_utils/io_utils_pf.py similarity index 94% rename from SimPEG/utils/io_utils/io_utils_pf.py rename to simpeg/utils/io_utils/io_utils_pf.py index 9387896481..ff689c1902 100644 --- a/SimPEG/utils/io_utils/io_utils_pf.py +++ b/simpeg/utils/io_utils/io_utils_pf.py @@ -16,9 +16,9 @@ def read_mag3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.magnetics.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.magnetics.survey.Survey`. """ # Prevent circular import @@ -89,9 +89,9 @@ def write_mag3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.magnetics.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.magnetics.survey.Survey` """ survey = data_object.survey @@ -136,9 +136,9 @@ def read_grav3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.gravity.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.gravity.survey.Survey`. """ # Prevent circular import @@ -201,9 +201,9 @@ def write_grav3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.gravity.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.gravity.survey.Survey` """ survey = data_object.survey src = survey.source_field @@ -244,9 +244,9 @@ def read_gg3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.gravity.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.gravity.survey.Survey`. """ # Prevent circular import @@ -262,7 +262,7 @@ def read_gg3d_ubc(obs_file): n_comp = len(components) factor = np.zeros(n_comp) - # Convert component types from UBC to SimPEG + # Convert component types from UBC to simpeg ubc_types = ["xx", "xy", "xz", "yy", "yz", "zz", "uv"] simpeg_types = ["gyy", "gxy", "gyz", "gxx", "gxz", "gzz", "guv"] factor_list = [1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0] @@ -328,14 +328,14 @@ def write_gg3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.gravity.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.gravity.survey.Survey` """ survey = data_object.survey src = survey.source_field - # Convert component types from UBC to SimPEG + # Convert component types from UBC to simpeg if len(src.receiver_list) > 1: raise NotImplementedError( "Writing of ubc format only supported for a single receiver." diff --git a/SimPEG/utils/mat_utils.py b/simpeg/utils/mat_utils.py similarity index 98% rename from SimPEG/utils/mat_utils.py rename to simpeg/utils/mat_utils.py index 46798e75dd..3614a15c2f 100644 --- a/SimPEG/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -134,12 +134,12 @@ def eigenvalue_by_power_iteration( r"""Estimate largest eigenvalue in absolute value using power iteration. Uses the power iteration approach to estimate the largest eigenvalue in absolute - value for a single :class:`SimPEG.BaseObjectiveFunction` or a combination of - objective functions stored in a :class:`SimPEG.ComboObjectiveFunction`. + value for a single :class:`simpeg.BaseObjectiveFunction` or a combination of + objective functions stored in a :class:`simpeg.ComboObjectiveFunction`. Parameters ---------- - combo_objfct : SimPEG.BaseObjectiveFunction + combo_objfct : simpeg.BaseObjectiveFunction Objective function or a combo objective function model : numpy.ndarray Current model @@ -148,7 +148,7 @@ def eigenvalue_by_power_iteration( fields_list : list (optional) ``list`` of fields objects for each data misfit term in combo_objfct. If none given, they will be evaluated within the function. If combo_objfct mixs data misfit and regularization - terms, the list should contains SimPEG.fields for the data misfit terms and None for the + terms, the list should contains simpeg.fields for the data misfit terms and None for the regularization term. seed : int Random seed for the initial random guess of eigenvector. diff --git a/SimPEG/utils/mesh_utils.py b/simpeg/utils/mesh_utils.py similarity index 100% rename from SimPEG/utils/mesh_utils.py rename to simpeg/utils/mesh_utils.py diff --git a/SimPEG/utils/model_builder.py b/simpeg/utils/model_builder.py similarity index 99% rename from SimPEG/utils/model_builder.py rename to simpeg/utils/model_builder.py index e9670319bf..285fa976c1 100644 --- a/SimPEG/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -440,7 +440,7 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) -------- >>> import matplotlib.pyplot as plt - >>> from SimPEG.utils.model_builder import create_random_model + >>> from simpeg.utils.model_builder import create_random_model >>> m = create_random_model((50,50), bounds=[-4,0]) >>> plt.colorbar(plt.imshow(m)) >>> plt.title('A very cool, yet completely random model.') diff --git a/SimPEG/utils/model_utils.py b/simpeg/utils/model_utils.py similarity index 100% rename from SimPEG/utils/model_utils.py rename to simpeg/utils/model_utils.py diff --git a/SimPEG/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py similarity index 99% rename from SimPEG/utils/pgi_utils.py rename to simpeg/utils/pgi_utils.py index eb0658e958..a11fe9fb56 100644 --- a/SimPEG/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -19,7 +19,7 @@ ) from sklearn.mixture._base import check_random_state, ConvergenceWarning import warnings -from SimPEG.maps import IdentityMap +from simpeg.maps import IdentityMap ############################################################################### diff --git a/SimPEG/utils/plot_utils.py b/simpeg/utils/plot_utils.py similarity index 100% rename from SimPEG/utils/plot_utils.py rename to simpeg/utils/plot_utils.py diff --git a/SimPEG/utils/solver_utils.py b/simpeg/utils/solver_utils.py similarity index 98% rename from SimPEG/utils/solver_utils.py rename to simpeg/utils/solver_utils.py index 12bd945b00..e5b4f343c4 100644 --- a/SimPEG/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -44,7 +44,7 @@ def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name= -------- A solver that does not have a factorize method. - >>> from SimPEG.utils.solver_utils import SolverWrapD + >>> from simpeg.utils.solver_utils import SolverWrapD >>> import scipy.sparse as sp >>> SpSolver = SolverWrapD(sp.linalg.spsolve, factorize=False) >>> A = sp.diags([1, -1], [0, 1], shape=(10, 10)) @@ -170,7 +170,7 @@ def SolverWrapI(fun, checkAccuracy=True, accuracyTol=1e-5, name=None): -------- >>> import scipy.sparse as sp - >>> from SimPEG.utils.solver_utils import SolverWrapI + >>> from simpeg.utils.solver_utils import SolverWrapI >>> SolverCG = SolverWrapI(sp.linalg.cg) >>> A = sp.diags([-1, 2, -1], [-1, 0, 1], shape=(10, 10)) @@ -278,7 +278,7 @@ class SolverDiag(object): Examples -------- >>> import scipy.sparse as sp - >>> from SimPEG.utils.solver_utils import SolverDiag + >>> from simpeg.utils.solver_utils import SolverDiag >>> A = sp.diags(np.linspace(1, 2, 10)) >>> b = np.arange(10) >>> Ainv = SolverDiag(A) diff --git a/tests/base/regularizations/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py index 907f04bb56..4b5741a7cb 100644 --- a/tests/base/regularizations/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -3,7 +3,7 @@ import numpy as np from discretize import TensorMesh, TreeMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) diff --git a/tests/base/regularizations/test_full_gradient.py b/tests/base/regularizations/test_full_gradient.py index a827676fc8..ae3b51e27f 100644 --- a/tests/base/regularizations/test_full_gradient.py +++ b/tests/base/regularizations/test_full_gradient.py @@ -2,7 +2,7 @@ from discretize.utils import example_simplex_mesh import discretize import numpy as np -from SimPEG.regularization import SmoothnessFullGradient +from simpeg.regularization import SmoothnessFullGradient import pytest diff --git a/tests/base/regularizations/test_jtv.py b/tests/base/regularizations/test_jtv.py index c016043da6..29239f36d7 100644 --- a/tests/base/regularizations/test_jtv.py +++ b/tests/base/regularizations/test_jtv.py @@ -4,7 +4,7 @@ import numpy as np from discretize import TensorMesh, TreeMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index cc0ce5ac94..b1f08e905f 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -6,9 +6,9 @@ from pymatsolver import SolverLU from scipy.stats import multivariate_normal -from SimPEG import regularization -from SimPEG.maps import Wires -from SimPEG.utils import WeightedGaussianMixture, mkvc +from simpeg import regularization +from simpeg.maps import Wires +from simpeg.utils import WeightedGaussianMixture, mkvc class TestPGI(unittest.TestCase): diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 779890667d..257dca9dba 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -5,8 +5,8 @@ import inspect import discretize -from SimPEG import maps, objective_function, regularization, utils -from SimPEG.regularization import ( +from simpeg import maps, objective_function, regularization, utils +from simpeg.regularization import ( BaseRegularization, WeightedLeastSquares, Sparse, @@ -15,7 +15,7 @@ SmoothnessFirstOrder, SmoothnessSecondOrder, ) -from SimPEG.objective_function import ComboObjectiveFunction +from simpeg.objective_function import ComboObjectiveFunction TOL = 1e-7 diff --git a/tests/base/test_Fields.py b/tests/base/test_Fields.py index 3c90616e1f..b9450bfc5f 100644 --- a/tests/base/test_Fields.py +++ b/tests/base/test_Fields.py @@ -1,7 +1,7 @@ import unittest import discretize -from SimPEG import survey, simulation, utils, fields, data +from simpeg import survey, simulation, utils, fields, data import numpy as np import sys diff --git a/tests/base/test_Props.py b/tests/base/test_Props.py index 6b718e2f4a..bb7de0602d 100644 --- a/tests/base/test_Props.py +++ b/tests/base/test_Props.py @@ -5,9 +5,9 @@ import discretize -from SimPEG import maps -from SimPEG import utils -from SimPEG import props +from simpeg import maps +from simpeg import utils +from simpeg import props class SimpleExample(props.HasModel): diff --git a/tests/base/test_Solver.py b/tests/base/test_Solver.py index 6c3423df3d..adb753f6c8 100644 --- a/tests/base/test_Solver.py +++ b/tests/base/test_Solver.py @@ -1,8 +1,8 @@ import unittest -from SimPEG import Solver, SolverDiag, SolverCG, SolverLU +from simpeg import Solver, SolverDiag, SolverCG, SolverLU from discretize import TensorMesh -from SimPEG.utils import sdiag +from simpeg.utils import sdiag import numpy as np TOLD = 1e-10 diff --git a/tests/base/test_coordutils.py b/tests/base/test_coordutils.py index 6ccd28ec43..601110ba2e 100644 --- a/tests/base/test_coordutils.py +++ b/tests/base/test_coordutils.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from SimPEG import utils +from simpeg import utils tol = 1e-15 diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index 64a8cbcd0a..e4cd6cac3f 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -3,7 +3,7 @@ import numpy as np from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) diff --git a/tests/base/test_data.py b/tests/base/test_data.py index 75947c8b06..b62d6cc9c0 100644 --- a/tests/base/test_data.py +++ b/tests/base/test_data.py @@ -3,9 +3,9 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG import simulation, survey -from SimPEG import Data +from simpeg import maps +from simpeg import simulation, survey +from simpeg import Data class DataTest(unittest.TestCase): diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index c83fea1291..2e23131da5 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -3,8 +3,8 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG import data_misfit, simulation, survey +from simpeg import maps +from simpeg import data_misfit, simulation, survey np.random.seed(17) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 2af5c58e32..474fba332a 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -3,7 +3,7 @@ import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, directives, regularization, @@ -13,7 +13,7 @@ inverse_problem, simulation, ) -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index 30033d3aa7..f856239edc 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -3,7 +3,7 @@ import numpy as np import discretize -from SimPEG import ( +from simpeg import ( data_misfit, maps, utils, @@ -13,7 +13,7 @@ directives, inversion, ) -from SimPEG.electromagnetics import resistivity as DC +from simpeg.electromagnetics import resistivity as DC np.random.seed(82) diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index f99cc4b23c..4957f8db28 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -4,7 +4,7 @@ import pytest import scipy.sparse as sp -from SimPEG import maps, models, utils +from simpeg import maps, models, utils from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import inspect diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_mass_matrices.py index 7dd1d82ebc..90dc04ea75 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_mass_matrices.py @@ -1,5 +1,5 @@ -from SimPEG.base import with_property_mass_matrices, BasePDESimulation -from SimPEG import props, maps +from simpeg.base import with_property_mass_matrices, BasePDESimulation +from simpeg import props, maps import unittest import discretize import numpy as np diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index 48279e4b54..97802fbf2d 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -5,7 +5,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( utils, ) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 211a9719bf..060dcf907c 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -3,10 +3,10 @@ import pytest import unittest -from SimPEG import utils, maps -from SimPEG import objective_function -from SimPEG.objective_function import _validate_multiplier -from SimPEG.utils import Zero +from simpeg import utils, maps +from simpeg import objective_function +from simpeg.objective_function import _validate_multiplier +from simpeg.utils import Zero np.random.seed(130) diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index 212d433787..45ef588f25 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -1,8 +1,8 @@ import unittest -from SimPEG.utils import sdiag +from simpeg.utils import sdiag import numpy as np import scipy.sparse as sp -from SimPEG import optimization +from simpeg import optimization from discretize.tests import get_quadratic, rosenbrock TOL = 1e-2 diff --git a/tests/base/test_problem.py b/tests/base/test_problem.py index c256c627da..a6afd10479 100644 --- a/tests/base/test_problem.py +++ b/tests/base/test_problem.py @@ -1,6 +1,6 @@ import unittest import discretize -from SimPEG import simulation +from simpeg import simulation import numpy as np diff --git a/tests/base/test_simulation.py b/tests/base/test_simulation.py index 03a59c7fd7..4f49d4c9fa 100644 --- a/tests/base/test_simulation.py +++ b/tests/base/test_simulation.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import maps, simulation +from simpeg import maps, simulation class TestLinearSimulation(unittest.TestCase): diff --git a/tests/base/test_stub.py b/tests/base/test_stub.py new file mode 100644 index 0000000000..1b830df195 --- /dev/null +++ b/tests/base/test_stub.py @@ -0,0 +1,14 @@ +import pytest + + +def test_SimPEG_import(): + with pytest.warns( + FutureWarning, + match="Importing `SimPEG` is deprecated. please import from `simpeg`.", + ): + from SimPEG import data + import SimPEG + import simpeg + + assert SimPEG is simpeg + assert data.__file__.endswith("simpeg/data.py") diff --git a/tests/base/test_survey_data.py b/tests/base/test_survey_data.py index 1056cfee26..9b06649e07 100644 --- a/tests/base/test_survey_data.py +++ b/tests/base/test_survey_data.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from SimPEG import survey, utils, data +from simpeg import survey, utils, data np.random.seed(100) diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index 88938828ca..1fa7ad6d39 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -4,7 +4,7 @@ import scipy.sparse as sp import os import shutil -from SimPEG.utils import ( +from simpeg.utils import ( sdiag, sub2ind, ndgrid, diff --git a/tests/base/test_validators.py b/tests/base/test_validators.py index 5d35a2c5a2..2d2df0eb87 100644 --- a/tests/base/test_validators.py +++ b/tests/base/test_validators.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from SimPEG.utils import ( +from simpeg.utils import ( validate_string, validate_integer, validate_float, diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index 55722e9f67..a2f32c2e16 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -1,8 +1,8 @@ import unittest import numpy as np import discretize -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( maps, data_misfit, regularization, @@ -11,8 +11,8 @@ inverse_problem, tests, ) -from SimPEG.utils import mkvc -from SimPEG.electromagnetics import resistivity as dc +from simpeg.utils import mkvc +from simpeg.electromagnetics import resistivity as dc import shutil np.random.seed(40) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index e9dbece1a3..73ac660054 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -6,8 +6,8 @@ import discretize as ds import numpy as np -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( data_misfit, inverse_problem, inversion, @@ -17,9 +17,9 @@ tests, utils, ) -from SimPEG.electromagnetics import induced_polarization as ip -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc +from simpeg.electromagnetics import induced_polarization as ip +from simpeg.electromagnetics import resistivity as dc +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc np.random.seed(30) diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index 8d35014b9f..df68680167 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -3,8 +3,8 @@ import discretize from discretize.utils import active_from_xyz import dask -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( utils, maps, regularization, @@ -14,7 +14,7 @@ directives, inversion, ) -from SimPEG.potential_fields import gravity +from simpeg.potential_fields import gravity import shutil diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 37b8783745..cc3984f2ab 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( directives, maps, inverse_problem, @@ -14,7 +14,7 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import numpy as np -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 098423b4f8..0bf5c4b6e0 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( directives, maps, inverse_problem, @@ -15,7 +15,7 @@ import shutil -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import numpy as np diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 758db82d0a..eb1775c032 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -1,6 +1,6 @@ import numpy as np -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( data, data_misfit, directives, @@ -11,9 +11,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import unittest import shutil @@ -86,7 +86,7 @@ def setUp(self): )[0] # Assign magnetization value, inducing field strength will - # be applied in by the :class:`SimPEG.PF.Magnetics` problem + # be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e diff --git a/tests/em/em1d/test_EM1D_FD_fwd.py b/tests/em/em1d/test_EM1D_FD_fwd.py index f7c79ebaa5..8a62a1398f 100644 --- a/tests/em/em1d/test_EM1D_FD_fwd.py +++ b/tests/em/em1d/test_EM1D_FD_fwd.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG import maps +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps import numpy as np from scipy.constants import mu_0 from geoana.em.fdem import ( diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index 630e45a4d8..432b647a64 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests, TensorMesh -import SimPEG.electromagnetics.frequency_domain as fdem +import simpeg.electromagnetics.frequency_domain as fdem import numpy as np from scipy.constants import mu_0 diff --git a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py index 7f8e92f9a4..f5e3252857 100644 --- a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.electromagnetics.utils import convolve_with_waveform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils import convolve_with_waveform from geoana.em.tdem import ( vertical_magnetic_flux_time_deriv_horizontal_loop as dbdt_loop, ) diff --git a/tests/em/em1d/test_EM1D_TD_general_fwd.py b/tests/em/em1d/test_EM1D_TD_general_fwd.py index 6ea353b9cb..1d4daf5c6d 100644 --- a/tests/em/em1d/test_EM1D_TD_general_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_general_fwd.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.electromagnetics.utils import convolve_with_waveform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils import convolve_with_waveform from geoana.em.tdem import ( vertical_magnetic_flux_horizontal_loop as b_loop, vertical_magnetic_flux_time_deriv_horizontal_loop as dbdt_loop, diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index dd90a32a72..6cce1655aa 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -1,8 +1,8 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests import numpy as np -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem class EM1D_TD_general_Jac_layers_ProblemTests(unittest.TestCase): diff --git a/tests/em/em1d/test_EM1D_TD_off_fwd.py b/tests/em/em1d/test_EM1D_TD_off_fwd.py index 78b45f61ab..8a1078b02a 100644 --- a/tests/em/em1d/test_EM1D_TD_off_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_off_fwd.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem from scipy.constants import mu_0 from geoana.em.tdem import ( magnetic_flux_vertical_magnetic_dipole as b_dipole, diff --git a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py index 4345ffb426..c985b8e758 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests, TensorMesh -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem import numpy as np from scipy.constants import mu_0 diff --git a/tests/em/fdem/forward/test_FDEM_analytics.py b/tests/em/fdem/forward/test_FDEM_analytics.py index c021251b5c..afda665b43 100644 --- a/tests/em/fdem/forward/test_FDEM_analytics.py +++ b/tests/em/fdem/forward/test_FDEM_analytics.py @@ -5,9 +5,9 @@ import numpy as np import scipy.sparse as sp from scipy.constants import mu_0 -from SimPEG import SolverLU, utils -from SimPEG.electromagnetics import analytics -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import SolverLU, utils +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import frequency_domain as fdem # import matplotlib # matplotlib.use('Agg') diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index 549b66109e..f1aa75a736 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -1,6 +1,6 @@ -from SimPEG import tests, utils +from simpeg import tests, utils import numpy as np -import SimPEG.electromagnetics.analytics.FDEMcasing as Casing +import simpeg.electromagnetics.analytics.FDEMcasing as Casing import unittest from scipy.constants import mu_0 diff --git a/tests/em/fdem/forward/test_FDEM_forward.py b/tests/em/fdem/forward/test_FDEM_forward.py index 111edac6df..be81c2891e 100644 --- a/tests/em/fdem/forward/test_FDEM_forward.py +++ b/tests/em/fdem/forward/test_FDEM_forward.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEB = True testHJ = True diff --git a/tests/em/fdem/forward/test_FDEM_forwardEJHB.py b/tests/em/fdem/forward/test_FDEM_forwardEJHB.py index e7dd8aeb7f..c8c21957cf 100644 --- a/tests/em/fdem/forward/test_FDEM_forwardEJHB.py +++ b/tests/em/fdem/forward/test_FDEM_forwardEJHB.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEJ = True testBH = True diff --git a/tests/em/fdem/forward/test_FDEM_forwardHB.py b/tests/em/fdem/forward/test_FDEM_forwardHB.py index a55addc608..51c04608c2 100644 --- a/tests/em/fdem/forward/test_FDEM_forwardHB.py +++ b/tests/em/fdem/forward/test_FDEM_forwardHB.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEB = True testHJ = True diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index ce8a432b1d..c66b688bd0 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -2,8 +2,8 @@ # matplotlib.use('Agg') import discretize -from SimPEG import maps, tests, utils -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, tests, utils +from simpeg.electromagnetics import frequency_domain as fdem from pymatsolver import Pardiso as Solver diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index 4b499655c3..7ba8de8170 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -6,8 +6,8 @@ import numpy as np from geoana.em.static import MagneticDipoleWholeSpace from scipy.constants import mu_0 -from SimPEG import maps, utils -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, utils +from simpeg.electromagnetics import frequency_domain as fdem TOL = 0.5 # relative tolerance (to norm of soln) plotIt = False diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index a7d5f61fc1..2ef4abd272 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -5,7 +5,7 @@ import geoana import discretize -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg.electromagnetics import frequency_domain as fdem from pymatsolver import Pardiso diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index 5384c69d17..2b20b8c900 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.electromagnetics import time_domain as tdem def test_removed_projcomp(): diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py index 814cefebaa..618d133b5b 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testE = True testB = True diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py index 6225671fd2..4713ff4611 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testJ = True testH = True diff --git a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py index dc02014c59..7434ade3e1 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -1,8 +1,8 @@ import unittest import numpy as np -from SimPEG import tests +from simpeg import tests from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testE = False testB = True diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 0ebd99fd40..50864fa9e3 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -1,6 +1,6 @@ import discretize -from SimPEG import maps, utils, tests -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, utils, tests +from simpeg.electromagnetics import frequency_domain as fdem import numpy as np import unittest diff --git a/tests/em/nsem/forward/test_1D_finite_volume.py b/tests/em/nsem/forward/test_1D_finite_volume.py index 68940b68cc..4af4dae02f 100644 --- a/tests/em/nsem/forward/test_1D_finite_volume.py +++ b/tests/em/nsem/forward/test_1D_finite_volume.py @@ -1,7 +1,7 @@ import numpy as np from discretize import TensorMesh -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps from pymatsolver import Pardiso import unittest diff --git a/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py b/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py index df50db86fe..cab4338952 100644 --- a/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py +++ b/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py @@ -1,6 +1,6 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import discretize +from simpeg.electromagnetics import natural_source as nsem +from simpeg import discretize import numpy as np diff --git a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py index 2568f37078..a2432f9135 100644 --- a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py +++ b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py @@ -1,6 +1,6 @@ import unittest -from SimPEG import mkvc -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import mkvc +from simpeg.electromagnetics import natural_source as nsem import numpy as np # Define the tolerances diff --git a/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py index 5e4e1cea8f..af847a3b7f 100644 --- a/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem import numpy as np # Define the tolerances diff --git a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py index ea089c884a..5086c62d43 100644 --- a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py +++ b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py @@ -2,7 +2,7 @@ from scipy.constants import mu_0 import numpy as np -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem np.random.seed(1100) diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 43936faf02..16395302a5 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -1,6 +1,6 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps import numpy as np from scipy.constants import mu_0 diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index c1063656c4..10edb4ce3d 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -3,8 +3,8 @@ from scipy.constants import mu_0 from discretize.tests import check_derivative -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps from discretize import TensorMesh, TreeMesh, CylindricalMesh from pymatsolver import Pardiso diff --git a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py index 1dc3bed7c8..d3a9aaaafc 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py @@ -2,8 +2,8 @@ import unittest from scipy.constants import mu_0 -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps TOL = 1e-4 diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index e9ef878392..733e5bda1f 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -1,8 +1,8 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG import maps, tests -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import maps, tests +from simpeg.electromagnetics import natural_source as nsem TOL = 1e-4 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py index 7351fc9ef0..ecafadd8d0 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py @@ -1,6 +1,6 @@ import numpy as np import unittest -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index cd96b8127a..9d4b332a06 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -2,8 +2,8 @@ import pytest import unittest import numpy as np -from SimPEG import tests, mkvc -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import tests, mkvc +from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 TOLr = 5e-2 diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 07f42633bc..45cc6ef6cc 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -1,10 +1,10 @@ import unittest -# import SimPEG.dask as simpeg -from SimPEG import maps, tests +# import simpeg.dask as simpeg +from simpeg import maps, tests import discretize from discretize.utils import mkvc -from SimPEG.electromagnetics import natural_source as ns +from simpeg.electromagnetics import natural_source as ns import numpy as np from pymatsolver import Pardiso as Solver from discretize.utils import volume_average diff --git a/tests/em/nsem/survey/test_nsem_data.py b/tests/em/nsem/survey/test_nsem_data.py index 35f15f51da..61babc3818 100644 --- a/tests/em/nsem/survey/test_nsem_data.py +++ b/tests/em/nsem/survey/test_nsem_data.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG.electromagnetics.natural_source.survey import Data +from simpeg.electromagnetics.natural_source.survey import Data class TestNSEMData: diff --git a/tests/em/nsem/utils/test_data_utils.py b/tests/em/nsem/utils/test_data_utils.py index f3ecf4a62d..533f327495 100644 --- a/tests/em/nsem/utils/test_data_utils.py +++ b/tests/em/nsem/utils/test_data_utils.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG.electromagnetics.natural_source.utils.data_utils import rec_to_ndarr +from simpeg.electromagnetics.natural_source.utils.data_utils import rec_to_ndarr def test_rec_to_ndarr(): diff --git a/tests/em/static/test_DCIP_io_utils.py b/tests/em/static/test_DCIP_io_utils.py index 76bdd99319..12bd6c6beb 100644 --- a/tests/em/static/test_DCIP_io_utils.py +++ b/tests/em/static/test_DCIP_io_utils.py @@ -3,10 +3,10 @@ import unittest import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils -from SimPEG import data -from SimPEG.utils.io_utils import io_utils_electromagnetics as io_utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils +from simpeg import data +from simpeg.utils.io_utils import io_utils_electromagnetics as io_utils import shutil import os diff --git a/tests/em/static/test_DC_1D_jvecjtvecadj.py b/tests/em/static/test_DC_1D_jvecjtvecadj.py index d867543164..87ff631371 100644 --- a/tests/em/static/test_DC_1D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_1D_jvecjtvecadj.py @@ -1,10 +1,10 @@ from discretize.tests import check_derivative, assert_isadjoint import numpy as np import pytest -from SimPEG import ( +from simpeg import ( maps, ) -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index 7d465b6d87..9edbf3ca0f 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -3,9 +3,9 @@ from discretize import TensorMesh -from SimPEG import utils, SolverLU -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import analytics +from simpeg import utils, SolverLU +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import analytics class DCProblemAnalyticTests_DPDP(unittest.TestCase): diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index 0dcbb887db..c25e06711f 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -11,12 +11,12 @@ inversion, inverse_problem, ) -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class DCProblem_2DTests(unittest.TestCase): diff --git a/tests/em/static/test_DC_Boundaries.py b/tests/em/static/test_DC_Boundaries.py index 713730e3df..6bffbbd9ef 100644 --- a/tests/em/static/test_DC_Boundaries.py +++ b/tests/em/static/test_DC_Boundaries.py @@ -3,7 +3,7 @@ import discretize from discretize.utils import example_simplex_mesh -import SimPEG.electromagnetics.static.resistivity as dc +import simpeg.electromagnetics.static.resistivity as dc tens_2d = discretize.TensorMesh([8, 9]) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index 4257187a29..ac33a2f2b9 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -1,14 +1,14 @@ import unittest from discretize import TensorMesh -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index dd725b6d04..d5e9b8ebfc 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -1,14 +1,14 @@ import unittest from discretize import TensorMesh -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 3d09ea1ecd..e8b3e133a5 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -4,17 +4,17 @@ import numpy as np import discretize -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils -from SimPEG import maps, mkvc -from SimPEG.utils import io_utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils +from simpeg import maps, mkvc +from simpeg.utils import io_utils import shutil import os try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class DCUtilsTests_halfspace(unittest.TestCase): diff --git a/tests/em/static/test_DC_analytic.py b/tests/em/static/test_DC_analytic.py index 8c1d169135..d26c1a1cca 100644 --- a/tests/em/static/test_DC_analytic.py +++ b/tests/em/static/test_DC_analytic.py @@ -1,15 +1,15 @@ import unittest import discretize -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import analytics +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import analytics try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class DCProblemAnalyticTests(unittest.TestCase): diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 057e16c36c..25c08fc8e2 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, data_misfit, regularization, @@ -11,8 +11,8 @@ tests, utils, ) -from SimPEG.utils import mkvc -from SimPEG.electromagnetics import resistivity as dc +from simpeg.utils import mkvc +from simpeg.electromagnetics import resistivity as dc from pymatsolver import Pardiso import shutil diff --git a/tests/em/static/test_DC_miniaturize.py b/tests/em/static/test_DC_miniaturize.py index 95ec1d80b6..95a2fb67a7 100644 --- a/tests/em/static/test_DC_miniaturize.py +++ b/tests/em/static/test_DC_miniaturize.py @@ -1,6 +1,6 @@ -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import generate_dcip_sources_line -from SimPEG import maps +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import generate_dcip_sources_line +from simpeg import maps import numpy as np from pymatsolver import Pardiso import discretize diff --git a/tests/em/static/test_IO.py b/tests/em/static/test_IO.py index 791381af11..b6816ac067 100644 --- a/tests/em/static/test_IO.py +++ b/tests/em/static/test_IO.py @@ -1,8 +1,8 @@ # import matplotlib # matplotlib.use('Agg') -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils import matplotlib.pyplot as plt import numpy as np import unittest diff --git a/tests/em/static/test_IP_2D_fwd.py b/tests/em/static/test_IP_2D_fwd.py index ef416f2594..93c22b41e9 100644 --- a/tests/em/static/test_IP_2D_fwd.py +++ b/tests/em/static/test_IP_2D_fwd.py @@ -1,15 +1,15 @@ import unittest import discretize -from SimPEG import utils, maps +from simpeg import utils, maps import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class IPProblemAnalyticTests(unittest.TestCase): diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index a247f52981..c95da77982 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -2,17 +2,17 @@ import discretize import numpy as np -from SimPEG import utils -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import utils +from simpeg import maps +from simpeg import data_misfit +from simpeg import regularization +from simpeg import optimization +from simpeg import inversion +from simpeg import inverse_problem +from simpeg import tests + +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip np.random.seed(30) diff --git a/tests/em/static/test_IP_fwd.py b/tests/em/static/test_IP_fwd.py index 9f8f6e87d8..3d722f931c 100644 --- a/tests/em/static/test_IP_fwd.py +++ b/tests/em/static/test_IP_fwd.py @@ -3,14 +3,14 @@ import numpy as np import discretize -from SimPEG import utils, maps -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import utils, maps +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class IPProblemAnalyticTests(unittest.TestCase): diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index b1163280d1..8884b3b385 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -2,16 +2,16 @@ import discretize import numpy as np -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import maps +from simpeg import data_misfit +from simpeg import regularization +from simpeg import optimization +from simpeg import inversion +from simpeg import inverse_problem +from simpeg import tests + +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip import shutil np.random.seed(30) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 64fabc6851..751aa7ea98 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, @@ -12,12 +12,12 @@ tests, ) import numpy as np -from SimPEG.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics import spectral_induced_polarization as sip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(38) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index a55272f50e..d9400f6f1e 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -1,6 +1,6 @@ import unittest import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, @@ -11,12 +11,12 @@ tests, ) import numpy as np -from SimPEG.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics import spectral_induced_polarization as sip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(38) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index d884334404..d6b585ee50 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -1,10 +1,10 @@ import pytest import numpy as np -import SimPEG.electromagnetics.static.spontaneous_potential as sp -import SimPEG.electromagnetics.static.resistivity as dc +import simpeg.electromagnetics.static.spontaneous_potential as sp +import simpeg.electromagnetics.static.resistivity as dc import discretize -from SimPEG import utils -from SimPEG import maps +from simpeg import utils +from simpeg import maps from discretize.tests import check_derivative, assert_isadjoint diff --git a/tests/em/static/test_properties.py b/tests/em/static/test_properties.py index 3b4fb9202e..5a9c6419bb 100644 --- a/tests/em/static/test_properties.py +++ b/tests/em/static/test_properties.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import spectral_induced_polarization as sip +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import spectral_induced_polarization as sip def test_receiver_properties(): diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 1820044ca1..49d0b4476d 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -2,8 +2,8 @@ import numpy as np import time import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps, tests +from simpeg.electromagnetics import time_domain as tdem from pymatsolver import Pardiso as Solver diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index baeb1a50ad..e650dde269 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -2,9 +2,9 @@ import numpy as np import time import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +from simpeg import maps, tests +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import utils from scipy.interpolate import interp1d from pymatsolver import Pardiso as Solver import pytest diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 145a011b80..fb764d924b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -1,8 +1,8 @@ import unittest import numpy as np import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps, tests +from simpeg.electromagnetics import time_domain as tdem from pymatsolver import Pardiso as Solver plotIt = False diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index d6548bbd37..ec0742b066 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -1,9 +1,9 @@ import unittest import discretize -from SimPEG import maps -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +from simpeg import maps +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import utils import numpy as np from pymatsolver import Pardiso as Solver diff --git a/tests/em/tdem/test_TDEM_forward_Analytic.py b/tests/em/tdem/test_TDEM_forward_Analytic.py index 9594e3de86..1c6e85c18e 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic.py @@ -5,9 +5,9 @@ import numpy as np from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 -from SimPEG import maps -from SimPEG.electromagnetics import analytics -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import time_domain as tdem def analytic_wholespace_dipole_comparison( diff --git a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py index da6b4a3935..c5a8e9ba63 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py @@ -6,10 +6,10 @@ from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from scipy.interpolate import interp1d -from SimPEG import maps -from SimPEG.electromagnetics import analytics -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +from simpeg import maps +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import utils def halfSpaceProblemAnaDiff( diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index 116cfb00e2..c85a8807cc 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -2,10 +2,10 @@ from scipy.constants import mu_0 import unittest -# SimPEG, discretize +# simpeg, discretize import discretize -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG import maps, tests +from simpeg.electromagnetics import time_domain as tdem +from simpeg import maps, tests from pymatsolver import Pardiso diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index a6845b0a44..8bf243554d 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -7,8 +7,8 @@ from scipy.constants import mu_0 import time -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG import utils, maps +from simpeg.electromagnetics import time_domain as tdem +from simpeg import utils, maps from pymatsolver import Pardiso diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 8d3aa9b511..3d4fff3896 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -5,7 +5,7 @@ import scipy.sparse as sp from discretize.tests import check_derivative from numpy.testing import assert_array_almost_equal -from SimPEG.electromagnetics.time_domain.sources import ( +from simpeg.electromagnetics.time_domain.sources import ( CircularLoop, ExponentialWaveform, HalfSineWaveform, diff --git a/tests/em/tdem/test_properties.py b/tests/em/tdem/test_properties.py index 1c1bb50136..e1305b52da 100644 --- a/tests/em/tdem/test_properties.py +++ b/tests/em/tdem/test_properties.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from SimPEG.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import time_domain as tdem def test_removed_projcomp(): diff --git a/tests/em/utils/test_linecurrents.py b/tests/em/utils/test_linecurrents.py index e824dc76a1..f0d94f3cb3 100644 --- a/tests/em/utils/test_linecurrents.py +++ b/tests/em/utils/test_linecurrents.py @@ -1,12 +1,12 @@ import numpy as np -from SimPEG.electromagnetics.utils import ( +from simpeg.electromagnetics.utils import ( getStraightLineCurrentIntegral, segmented_line_current_source_term, line_through_faces, ) import discretize import unittest -from SimPEG.utils import download +from simpeg.utils import download class LineCurrentTests(unittest.TestCase): diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 7bcef1c9e0..5e45cdcb6f 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_fwd_tests(unittest.TestCase): diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 15c022c926..1b64627490 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -3,16 +3,16 @@ import discretize -from SimPEG import mkvc +from simpeg import mkvc -from SimPEG import data_misfit -from SimPEG import optimization -from SimPEG import regularization -from SimPEG import inverse_problem -from SimPEG import inversion -from SimPEG.directives import BetaSchedule, TargetMisfit +from simpeg import data_misfit +from simpeg import optimization +from simpeg import regularization +from simpeg import inverse_problem +from simpeg import inversion +from simpeg.directives import BetaSchedule, TargetMisfit -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_inversion_tests(unittest.TestCase): diff --git a/tests/em/vrm/test_waveform.py b/tests/em/vrm/test_waveform.py index 15f30b9132..a005fc744f 100644 --- a/tests/em/vrm/test_waveform.py +++ b/tests/em/vrm/test_waveform.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_waveform_tests(unittest.TestCase): diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 0c750a4fce..71e9cc31b0 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -4,14 +4,14 @@ from discretize.tests import check_derivative import discretize -from SimPEG import maps -from SimPEG import utils -from SimPEG.flow import richards +from simpeg import maps +from simpeg import utils +from simpeg.flow import richards try: from pymatsolver import Pardiso as Solver except Exception: - from SimPEG import Solver + from simpeg import Solver TOL = 1e-8 diff --git a/tests/flow/test_Richards_empirical.py b/tests/flow/test_Richards_empirical.py index 44bcf00d8f..4361621e3f 100644 --- a/tests/flow/test_Richards_empirical.py +++ b/tests/flow/test_Richards_empirical.py @@ -5,8 +5,8 @@ import discretize from discretize.tests import check_derivative -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards TOL = 1e-8 diff --git a/tests/meta/test_dask_meta.py b/tests/meta/test_dask_meta.py index ac44d70a1a..5feb5f6c75 100644 --- a/tests/meta/test_dask_meta.py +++ b/tests/meta/test_dask_meta.py @@ -1,12 +1,12 @@ import numpy as np -from SimPEG.potential_fields import gravity -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG import maps +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps from discretize import TensorMesh import scipy.sparse as sp import pytest -from SimPEG.meta import ( +from simpeg.meta import ( MetaSimulation, SumMetaSimulation, RepeatedSimulation, diff --git a/tests/meta/test_meta_sim.py b/tests/meta/test_meta_sim.py index 2498aeaa36..bb88591156 100644 --- a/tests/meta/test_meta_sim.py +++ b/tests/meta/test_meta_sim.py @@ -1,12 +1,12 @@ import numpy as np -from SimPEG.potential_fields import gravity -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG import maps +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps from discretize import TensorMesh import scipy.sparse as sp import pytest -from SimPEG.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation +from simpeg.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation def test_multi_sim_correctness(): diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index fc1058862d..eaabf64f6f 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -2,13 +2,13 @@ import multiprocessing as mp import sys -from SimPEG.potential_fields import gravity -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG import maps +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps from discretize import TensorMesh import scipy.sparse as sp -from SimPEG.meta import ( +from simpeg.meta import ( MetaSimulation, SumMetaSimulation, RepeatedSimulation, diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 5e7d865f6b..40aff12b53 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,8 +1,8 @@ import pytest import discretize -import SimPEG -from SimPEG import maps -from SimPEG.potential_fields import gravity +import simpeg +from simpeg import maps +from simpeg.potential_fields import gravity from geoana.gravity import Prism import numpy as np @@ -404,8 +404,8 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): """ Check if error is raised when choclo is missing and chosen as engine. """ - # Monkeypatch choclo in SimPEG.potential_fields.base - monkeypatch.setattr(SimPEG.potential_fields.gravity.simulation, "choclo", None) + # Monkeypatch choclo in simpeg.potential_fields.base + monkeypatch.setattr(simpeg.potential_fields.gravity.simulation, "choclo", None) # Check if error is raised msg = "The choclo package couldn't be found." with pytest.raises(ImportError, match=msg): diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 9525edd4a1..21f0570189 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -6,8 +6,8 @@ from geoana.em.static import MagneticPrism from scipy.constants import mu_0 -from SimPEG import maps, utils -from SimPEG.potential_fields import magnetics as mag +from simpeg import maps, utils +from simpeg.potential_fields import magnetics as mag def test_ana_mag_forward(): diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index b0914da781..660af07d3f 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -1,8 +1,8 @@ import unittest import discretize -from SimPEG import utils, maps -from SimPEG.utils.model_builder import get_indices_sphere -from SimPEG.potential_fields import magnetics as mag +from simpeg import utils, maps +from simpeg.utils.model_builder import get_indices_sphere +from simpeg.potential_fields import magnetics as mag import numpy as np from pymatsolver import Pardiso diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index c67a8ba474..3657c4668d 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -3,7 +3,7 @@ import discretize from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -13,7 +13,7 @@ directives, inversion, ) -from SimPEG.potential_fields import gravity +from simpeg.potential_fields import gravity @pytest.mark.parametrize("engine", ("geoana", "choclo")) diff --git a/tests/pf/test_gravity_IO.py b/tests/pf/test_gravity_IO.py index 972018f64b..0431047e3d 100644 --- a/tests/pf/test_gravity_IO.py +++ b/tests/pf/test_gravity_IO.py @@ -1,9 +1,9 @@ import unittest import numpy as np -# from SimPEG.potential_fields import gravity -from SimPEG.utils.drivers import GravityDriver_Inv -from SimPEG.utils import io_utils +# from simpeg.potential_fields import gravity +from simpeg.utils.drivers import GravityDriver_Inv +from simpeg.utils import io_utils import shutil import os diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 1880095af8..5fc3fc68c2 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -1,5 +1,5 @@ import unittest -from SimPEG import ( +from simpeg import ( directives, maps, inverse_problem, @@ -13,7 +13,7 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import numpy as np -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index bf7e10dba8..2e766c7478 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -1,7 +1,7 @@ import unittest import discretize from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -13,8 +13,8 @@ ) import numpy as np -# import SimPEG.PF as PF -from SimPEG.potential_fields import magnetics as mag +# import simpeg.PF as PF +from simpeg.potential_fields import magnetics as mag import shutil diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index 8167bf1e1f..a78e714e2e 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -3,7 +3,7 @@ import numpy as np from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz -from SimPEG import ( +from simpeg import ( directives, maps, inverse_problem, @@ -13,7 +13,7 @@ utils, regularization, ) -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag class MagInvLinProblemTest(unittest.TestCase): diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index 85f27266d6..d4f11f6ce8 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -10,9 +10,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import unittest import shutil @@ -85,7 +85,7 @@ def setUp(self): )[0] # Assign magnetization value, inducing field strength will - # be applied in by the :class:`SimPEG.PF.Magnetics` problem + # be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index d4e72bae40..785be3355e 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -3,7 +3,7 @@ """ import pytest -from SimPEG.potential_fields.magnetics import UniformBackgroundField, SourceField +from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField def test_invalid_parameters_argument(): diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 5dea5ded25..65cdde4ddc 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -1,5 +1,5 @@ import unittest -from SimPEG import ( +from simpeg import ( directives, maps, inverse_problem, @@ -13,7 +13,7 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import numpy as np -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil diff --git a/tests/pf/test_magnetics_IO.py b/tests/pf/test_magnetics_IO.py index 2d686453ce..aa0e386619 100644 --- a/tests/pf/test_magnetics_IO.py +++ b/tests/pf/test_magnetics_IO.py @@ -1,9 +1,9 @@ import unittest import numpy as np -# from SimPEG import Mesh, PF -from SimPEG.utils.drivers import MagneticsDriver_Inv -from SimPEG.utils import io_utils +# from simpeg import Mesh, PF +from simpeg.utils.drivers import MagneticsDriver_Inv +from simpeg.utils import io_utils # from scipy.constants import mu_0 import shutil diff --git a/tests/pf/test_magnetics_analytics.py b/tests/pf/test_magnetics_analytics.py index 0a53870aa8..c93ac3c27e 100644 --- a/tests/pf/test_magnetics_analytics.py +++ b/tests/pf/test_magnetics_analytics.py @@ -1,9 +1,9 @@ import unittest -# from SimPEG import Mesh, PF +# from simpeg import Mesh, PF import discretize -from SimPEG.potential_fields import magnetics as mag -from SimPEG.utils.model_builder import get_indices_sphere +from simpeg.potential_fields import magnetics as mag +from simpeg.utils.model_builder import get_indices_sphere import numpy as np from scipy.constants import mu_0 diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 46bcf77c67..c6c7f64d8f 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -5,7 +5,7 @@ from discretize import TensorMesh from discretize.utils import mesh_builder_xyz, mkvc, refine_tree_xyz -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, @@ -15,7 +15,7 @@ regularization, utils, ) -from SimPEG.potential_fields import gravity, magnetics +from simpeg.potential_fields import gravity, magnetics class QuadTreeLinProblemTest(unittest.TestCase): diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index 53c96c96cb..eb39c97485 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -6,9 +6,9 @@ # import discretize # from pymatsolver import Pardiso # #import simpeg.PF as PF -# from SimPEG import maps, utils -# from SimPEG.potential_fields import magnetics as mag -# from SimPEG.utils.model_builder import get_indices_sphere +# from simpeg import maps, utils +# from simpeg.potential_fields import magnetics as mag +# from simpeg.utils.model_builder import get_indices_sphere # from scipy.constants import mu_0 # # diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py index cea965eb0b..84aa334561 100644 --- a/tests/pf/test_survey_counting.py +++ b/tests/pf/test_survey_counting.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG.potential_fields import gravity as grav -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import gravity as grav +from simpeg.potential_fields import magnetics as mag def test_gravity_survey(): diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index ae9d8cd16e..e7125d70fa 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -2,8 +2,8 @@ import unittest import discretize -from SimPEG.seismic import straight_ray_tomography as tomo -from SimPEG import tests, maps, utils +from simpeg.seismic import straight_ray_tomography as tomo +from simpeg import tests, maps, utils TOL = 1e-5 FLR = 1e-14 diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index cd41344e2e..924ee56e87 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -8,98 +8,98 @@ locs = np.array([[1.0, 2.0, 3.0]]) deprecated_modules = [ - # "SimPEG.utils.codeutils", - # "SimPEG.utils.coordutils", - # "SimPEG.utils.CounterUtils", - # "SimPEG.utils.curvutils", - # "SimPEG.utils.matutils", - # "SimPEG.utils.meshutils", - # "SimPEG.utils.ModelBuilder", - # "SimPEG.utils.PlotUtils", - # "SimPEG.utils.SolverUtils", - # "SimPEG.electromagnetics.utils.EMUtils", - # "SimPEG.electromagnetics.utils.AnalyticUtils", - # "SimPEG.electromagnetics.utils.CurrentUtils", - # "SimPEG.electromagnetics.utils.testingUtils", - # "SimPEG.electromagnetics.static.utils.StaticUtils", - # "SimPEG.electromagnetics.natural_source.utils.dataUtils", - # "SimPEG.electromagnetics.natural_source.utils.ediFilesUtils", - # "SimPEG.electromagnetics.natural_source.utils.MT1Danalytic", - # "SimPEG.electromagnetics.natural_source.utils.MT1Dsolutions", - # "SimPEG.electromagnetics.natural_source.utils.plotDataTypes", - # "SimPEG.electromagnetics.natural_source.utils.plotUtils", - # "SimPEG.electromagnetics.natural_source.utils.sourceUtils", - # "SimPEG.electromagnetics.natural_source.utils.testUtils", + # "simpeg.utils.codeutils", + # "simpeg.utils.coordutils", + # "simpeg.utils.CounterUtils", + # "simpeg.utils.curvutils", + # "simpeg.utils.matutils", + # "simpeg.utils.meshutils", + # "simpeg.utils.ModelBuilder", + # "simpeg.utils.PlotUtils", + # "simpeg.utils.SolverUtils", + # "simpeg.electromagnetics.utils.EMUtils", + # "simpeg.electromagnetics.utils.AnalyticUtils", + # "simpeg.electromagnetics.utils.CurrentUtils", + # "simpeg.electromagnetics.utils.testingUtils", + # "simpeg.electromagnetics.static.utils.StaticUtils", + # "simpeg.electromagnetics.natural_source.utils.dataUtils", + # "simpeg.electromagnetics.natural_source.utils.ediFilesUtils", + # "simpeg.electromagnetics.natural_source.utils.MT1Danalytic", + # "simpeg.electromagnetics.natural_source.utils.MT1Dsolutions", + # "simpeg.electromagnetics.natural_source.utils.plotDataTypes", + # "simpeg.electromagnetics.natural_source.utils.plotUtils", + # "simpeg.electromagnetics.natural_source.utils.sourceUtils", + # "simpeg.electromagnetics.natural_source.utils.testUtils", ] deprecated_problems = [ # [ - # "SimPEG.electromagnetics.frequency_domain", + # "simpeg.electromagnetics.frequency_domain", # ("Problem3D_e", "Problem3D_b", "Problem3D_h", "Problem3D_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain", + # "simpeg.electromagnetics.time_domain", # ("Problem3D_e", "Problem3D_b", "Problem3D_h", "Problem3D_j"), # ], # [ - # "SimPEG.electromagnetics.natural_source", + # "simpeg.electromagnetics.natural_source", # ("Problem3D_ePrimSec", "Problem1D_ePrimSec"), # ], # [ - # "SimPEG.electromagnetics.static.induced_polarization", + # "simpeg.electromagnetics.static.induced_polarization", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.static.resistivity", + # "simpeg.electromagnetics.static.resistivity", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.static.spectral_induced_polarization", + # "simpeg.electromagnetics.static.spectral_induced_polarization", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.viscous_remanent_magnetization", + # "simpeg.electromagnetics.viscous_remanent_magnetization", # ("Problem_Linear", "Problem_LogUnifrom"), # ], ] deprecated_fields = [ # [ - # "SimPEG.electromagnetics.frequency_domain", + # "simpeg.electromagnetics.frequency_domain", # ("Fields3D_e", "Fields3D_b", "Fields3D_h", "Fields3D_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain", + # "simpeg.electromagnetics.time_domain", # ("Fields3D_e", "Fields3D_b", "Fields3D_h", "Fields3D_j"), # ], # [ - # "SimPEG.electromagnetics.natural_source", + # "simpeg.electromagnetics.natural_source", # ("Fields1D_ePrimSec", "Fields3D_ePrimSec"), # ], # [ - # "SimPEG.electromagnetics.static.resistivity", + # "simpeg.electromagnetics.static.resistivity", # ("Fields_CC", "Fields_N", "Fields_ky", "Fields_ky_CC", "Fields_ky_N"), # ], ] deprecated_receivers = [ # [ - # "SimPEG.electromagnetics.frequency_domain.receivers", + # "simpeg.electromagnetics.frequency_domain.receivers", # ("Point_e", "Point_b", "Point_bSecondary", "Point_h", "Point_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain.receivers", + # "simpeg.electromagnetics.time_domain.receivers", # ("Point_e", "Point_b", "Point_h", "Point_j", "Point_dbdt", "Point_dhdt"), # ], # [ - # "SimPEG.electromagnetics.natural_source.receivers", + # "simpeg.electromagnetics.natural_source.receivers", # ("Point_impedance1D", "Point_impedance3D", "Point_tipper3D"), # ], - # ["SimPEG.electromagnetics.static.resistivity.receivers", ("Dipole_ky", "Pole_ky")], + # ["simpeg.electromagnetics.static.resistivity.receivers", ("Dipole_ky", "Pole_ky")], ] deprcated_surveys = [ - # "SimPEG.electromagnetics.static.resistivity", ("Survey") + # "simpeg.electromagnetics.static.resistivity", ("Survey") ] diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py index a262d46a4e..dbef02a7f5 100644 --- a/tests/utils/test_gmm_utils.py +++ b/tests/utils/test_gmm_utils.py @@ -1,8 +1,8 @@ import numpy as np import unittest import discretize -from SimPEG.maps import Wires -from SimPEG.utils import ( +from simpeg.maps import Wires +from simpeg.utils import ( mkvc, WeightedGaussianMixture, GaussianMixtureWithPrior, diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 76159b6a6a..9e9a07f206 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -1,10 +1,10 @@ import unittest import numpy as np -from SimPEG.data import Data -from SimPEG.potential_fields import gravity, magnetics -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils.io_utils import ( +from simpeg.data import Data +from simpeg.potential_fields import gravity, magnetics +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils.io_utils import ( write_grav3d_ubc, read_grav3d_ubc, write_gg3d_ubc, diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 30655046b0..3c8e1db1fd 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -2,10 +2,10 @@ import numpy as np from scipy.sparse.linalg import eigsh from discretize import TensorMesh -from SimPEG import simulation, data_misfit -from SimPEG.maps import IdentityMap -from SimPEG.regularization import WeightedLeastSquares -from SimPEG.utils.mat_utils import eigenvalue_by_power_iteration +from simpeg import simulation, data_misfit +from simpeg.maps import IdentityMap +from simpeg.regularization import WeightedLeastSquares +from simpeg.utils.mat_utils import eigenvalue_by_power_iteration class TestEigenvalues(unittest.TestCase): @@ -39,11 +39,11 @@ def g(k): true_model[mesh.cell_centers_x > 0.6] = 0 self.true_model = true_model - # Create a SimPEG simulation + # Create a simpeg simulation model_map = IdentityMap(mesh) sim = simulation.LinearSimulation(mesh, G=G, model_map=model_map) - # Create a SimPEG data object + # Create a simpeg data object relative_error = 0.1 noise_floor = 1e-4 data_obj = sim.make_synthetic_data( diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index 9d5091545e..828d06ec3f 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -1,6 +1,6 @@ import scooby import unittest -from SimPEG import Report +from simpeg import Report class TestReport(unittest.TestCase): @@ -10,7 +10,7 @@ def test_version_defaults(self): out1 = Report() out2 = scooby.Report( core=[ - "SimPEG", + "simpeg", "discretize", "pymatsolver", "numpy", diff --git a/tests/utils/test_solverwrap.py b/tests/utils/test_solverwrap.py index 9a56192a7b..126f4466ee 100644 --- a/tests/utils/test_solverwrap.py +++ b/tests/utils/test_solverwrap.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag +from simpeg.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag import scipy.sparse as sp import numpy as np diff --git a/tutorials/01-models_mapping/plot_1_tensor_models.py b/tutorials/01-models_mapping/plot_1_tensor_models.py index 939c61aa69..ebcf496e95 100644 --- a/tutorials/01-models_mapping/plot_1_tensor_models.py +++ b/tutorials/01-models_mapping/plot_1_tensor_models.py @@ -20,8 +20,8 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import mkvc, model_builder -from SimPEG import maps +from simpeg.utils import mkvc, model_builder +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/tutorials/01-models_mapping/plot_2_cyl_models.py b/tutorials/01-models_mapping/plot_2_cyl_models.py index 09b24b54ad..cb118b43f9 100644 --- a/tutorials/01-models_mapping/plot_2_cyl_models.py +++ b/tutorials/01-models_mapping/plot_2_cyl_models.py @@ -21,8 +21,8 @@ # from discretize import CylindricalMesh -from SimPEG.utils import mkvc -from SimPEG import maps +from simpeg.utils import mkvc +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/tutorials/01-models_mapping/plot_3_tree_models.py b/tutorials/01-models_mapping/plot_3_tree_models.py index 2204bcb800..2a8549aa6a 100644 --- a/tutorials/01-models_mapping/plot_3_tree_models.py +++ b/tutorials/01-models_mapping/plot_3_tree_models.py @@ -21,8 +21,8 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import mkvc, model_builder -from SimPEG import maps +from simpeg.utils import mkvc, model_builder +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py b/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py index 6173c95c4c..2d284b4264 100644 --- a/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py +++ b/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py @@ -25,7 +25,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( simulation, maps, data_misfit, diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index 4241d5d886..4715d3937b 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -21,7 +21,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( simulation, maps, data_misfit, diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index 22721d3fcb..d2d8aacfd6 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -2,7 +2,7 @@ Forward Simulation of Gravity Anomaly Data on a Tensor Mesh =========================================================== -Here we use the module *SimPEG.potential_fields.gravity* to predict gravity +Here we use the module *simpeg.potential_fields.gravity* to predict gravity anomaly data for a synthetic density contrast model. The simulation is carried out on a tensor mesh. For this tutorial, we focus on the following: @@ -28,9 +28,9 @@ from discretize import TensorMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG import maps -from SimPEG.potential_fields import gravity +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import gravity save_output = False diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 7ebeea969e..84e4227cf3 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -2,7 +2,7 @@ Forward Simulation of Gradiometry Data on a Tree Mesh ===================================================== -Here we use the module *SimPEG.potential_fields.gravity* to predict gravity +Here we use the module *simpeg.potential_fields.gravity* to predict gravity gradiometry data for a synthetic density contrast model. The simulation is carried out on a tree mesh. For this tutorial, we focus on the following: @@ -26,9 +26,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG import maps -from SimPEG.potential_fields import gravity +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import gravity # sphinx_gallery_thumbnail_number = 2 diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 34fd346d83..89d043d65d 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -32,9 +32,9 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.utils import plot2Ddata, model_builder +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index cc658a6d27..7bfd6da31f 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -33,9 +33,9 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.utils import plot2Ddata, model_builder +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index af08030220..d5b90274fd 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -2,7 +2,7 @@ Forward Simulation of Total Magnetic Intensity Data =================================================== -Here we use the module *SimPEG.potential_fields.magnetics* to predict magnetic +Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic data for a magnetic susceptibility model. We simulate the data on a tensor mesh. For this tutorial, we focus on the following: @@ -27,9 +27,9 @@ from discretize import TensorMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG import maps -from SimPEG.potential_fields import magnetics +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import magnetics write_output = False diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 6ffe6ac691..0007708f83 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -2,7 +2,7 @@ Forward Simulation of Gradiometry Data for Magnetic Vector Models ================================================================= -Here we use the module *SimPEG.potential_fields.magnetics* to predict magnetic +Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic gradiometry data for magnetic vector models. The simulation is performed on a Tree mesh. For this tutorial, we focus on the following: @@ -27,9 +27,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata, model_builder, mat_utils -from SimPEG import maps -from SimPEG.potential_fields import magnetics +from simpeg.utils import plot2Ddata, model_builder, mat_utils +from simpeg import maps +from simpeg.potential_fields import magnetics # sphinx_gallery_thumbnail_number = 2 diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 7ff2f45463..24430e337a 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -34,9 +34,9 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.potential_fields import magnetics -from SimPEG.utils import plot2Ddata, model_builder -from SimPEG import ( +from simpeg.potential_fields import magnetics +from simpeg.utils import plot2Ddata, model_builder +from simpeg import ( maps, data, inverse_problem, diff --git a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py index b97cbad3b9..7ea20813c7 100644 --- a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py @@ -3,7 +3,7 @@ Simulate a 1D Sounding over a Layered Earth =========================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to predict +Here we use the module *simpeg.electromangetics.static.resistivity* to predict sounding data over a 1D layered Earth. In this tutorial, we focus on the following: - General definition of sources and receivers @@ -28,9 +28,9 @@ import matplotlib as mpl import matplotlib.pyplot as plt -from SimPEG import maps -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg import maps +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 747e60e024..8095504f90 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -3,7 +3,7 @@ DC Resistivity Forward Simulation in 2.5D ========================================= -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict DC resistivity data and plot using a pseudosection. In this tutorial, we focus on the following: @@ -24,11 +24,11 @@ from discretize import TreeMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc -from SimPEG import maps, data -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc +from simpeg import maps, data +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, apparent_resistivity_from_voltage, plot_pseudosection, @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_output = False mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index e0b7f377a2..ea7293d267 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -3,7 +3,7 @@ DC Resistivity Forward Simulation in 3D ======================================= -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict DC resistivity data on an OcTree mesh. In this tutorial, we focus on the following: - How to define the survey @@ -35,11 +35,11 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG import maps, data -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg import maps, data +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, apparent_resistivity_from_voltage, ) @@ -47,7 +47,7 @@ # To plot DC data in 3D, the user must have the plotly package try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -57,7 +57,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False diff --git a/tutorials/05-dcr/plot_gen_3_3d_to_2d.py b/tutorials/05-dcr/plot_gen_3_3d_to_2d.py index 6d7a87fbc9..8200698207 100644 --- a/tutorials/05-dcr/plot_gen_3_3d_to_2d.py +++ b/tutorials/05-dcr/plot_gen_3_3d_to_2d.py @@ -24,9 +24,9 @@ # -from SimPEG import utils -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg import utils +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, convert_survey_3d_to_2d_lines, plot_pseudosection, @@ -42,7 +42,7 @@ try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py index 03f950cee8..75deb01d18 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py @@ -3,7 +3,7 @@ Least-Squares 1D Inversion of Sounding Data =========================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +Here we use the module *simpeg.electromangetics.static.resistivity* to invert DC resistivity sounding data and recover a 1D electrical resistivity model. In this tutorial, we focus on the following: @@ -30,7 +30,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -41,8 +41,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py index 34e4989641..f461b716dd 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -3,7 +3,7 @@ Sparse 1D Inversion of Sounding Data ==================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +Here we use the module *simpeg.electromangetics.static.resistivity* to invert DC resistivity sounding data and recover a 1D electrical resistivity model. In this tutorial, we focus on the following: @@ -30,7 +30,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -41,8 +41,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py index ff8618dbc9..63936d4e9d 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py @@ -3,7 +3,7 @@ Parametric 1D Inversion of Sounding Data ======================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +Here we use the module *simpeg.electromangetics.static.resistivity* to invert DC resistivity sounding data and recover the resistivities and layer thicknesses for a 1D layered Earth. In this tutorial, we focus on the following: @@ -31,7 +31,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -42,8 +42,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index fd47d61bc2..f28bb8d26d 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -31,8 +31,8 @@ from discretize import TreeMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -42,16 +42,16 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import ( plot_pseudosection, ) -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 4 diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index a6a4024e1f..f84891fb15 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -31,8 +31,8 @@ from discretize import TreeMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -42,17 +42,17 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import ( plot_pseudosection, apparent_resistivity_from_voltage, ) -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 3 diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 34633b0e73..4ca93a6106 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -34,9 +34,9 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz +from simpeg import ( maps, data_misfit, regularization, @@ -46,15 +46,15 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, ) # To plot DC/IP data in 3D, the user must have the plotly package try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -64,7 +64,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index ada51a11ef..4e1add360e 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -3,8 +3,8 @@ 2.5D Forward Simulation of a DCIP Line ====================================== -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict -DC resistivity data and the module *SimPEG.electromagnetics.static.induced_polarization* +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict +DC resistivity data and the module *simpeg.electromagnetics.static.induced_polarization* to predict IP data for a dipole-dipole survey. In this tutorial, we focus on the following: @@ -32,12 +32,12 @@ from discretize import TreeMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc -from SimPEG import maps, data -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc +from simpeg import maps, data +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, plot_pseudosection, apparent_resistivity_from_voltage, @@ -52,7 +52,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index 3c522e531c..dc5a07db96 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -3,9 +3,9 @@ DC/IP Forward Simulation in 3D ============================== -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict DC resistivity data on an OcTree mesh. Then we use the module -*SimPEG.electromagnetics.static.induced_polarization* to predict IP data. +*simpeg.electromagnetics.static.induced_polarization* to predict IP data. In this tutorial, we focus on the following: - How to define the survey @@ -37,12 +37,12 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG import maps, data -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg import maps, data +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, apparent_resistivity_from_voltage, ) @@ -50,7 +50,7 @@ # To plot DC/IP data in 3D, the user must have the plotly package try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -60,7 +60,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index fe274ce764..72cf5c2639 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -35,8 +35,8 @@ from discretize import TreeMesh from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -46,18 +46,18 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, plot_pseudosection, ) -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index 39f8a9a9fd..94b4cd9407 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -35,9 +35,9 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz +from simpeg import ( maps, data_misfit, regularization, @@ -47,16 +47,16 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, ) # To plot DC/IP data in 3D, the user must have the plotly package try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -66,7 +66,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/07-fdem/plot_fwd_1_em1dfm.py b/tutorials/07-fdem/plot_fwd_1_em1dfm.py index bcb0254477..87afd6f83e 100644 --- a/tutorials/07-fdem/plot_fwd_1_em1dfm.py +++ b/tutorials/07-fdem/plot_fwd_1_em1dfm.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Single Sounding =========================================== -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to predict +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to predict frequency domain data for a single sounding over a 1D layered Earth. In this tutorial, we focus on the following: @@ -28,9 +28,9 @@ from matplotlib import pyplot as plt from discretize import TensorMesh -from SimPEG import maps -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.utils import plot_1d_layer_model +from simpeg import maps +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.utils import plot_1d_layer_model plt.rcParams.update({"font.size": 16}) write_output = False @@ -142,7 +142,7 @@ # The simulation requires the user define the survey, the layer thicknesses # and a mapping from the model to the conductivities of the layers. # -# When using the *SimPEG.electromagnetics.frequency_domain_1d* module, +# When using the *simpeg.electromagnetics.frequency_domain_1d* module, # predicted data are organized by source, then by receiver, then by frequency. # diff --git a/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py b/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py index ded6972add..d154916d7f 100644 --- a/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py +++ b/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Susceptible and Chargeable Earth ============================================================ -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to compare +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to compare predicted frequency domain data for a single sounding when the Earth is purely conductive, conductive and magnetically susceptible, and when it is chargeable. In this tutorial, we focus on: @@ -26,9 +26,9 @@ import numpy as np from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG.electromagnetics.utils.em1d_utils import ColeCole +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg.electromagnetics.utils.em1d_utils import ColeCole plt.rcParams.update({"font.size": 16}) @@ -161,7 +161,7 @@ # parameters used to define the physical properties are permanently set when # defining the simulation. # -# When using the *SimPEG.electromagnetics.frequency_domain_1d* module, note that +# When using the *simpeg.electromagnetics.frequency_domain_1d* module, note that # predicted data are organized by source, then by receiver, then by frequency. # # diff --git a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py index 2ccb719b5f..9bace19d92 100644 --- a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py +++ b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py @@ -2,7 +2,7 @@ 3D Forward Simulation on a Cylindrical Mesh =========================================== -Here we use the module *SimPEG.electromagnetics.frequency_domain* to simulate the +Here we use the module *simpeg.electromagnetics.frequency_domain* to simulate the FDEM response for a borehole survey using a cylindrical mesh and radially symmetric conductivity model. For this tutorial, we focus on the following: @@ -27,8 +27,8 @@ from discretize import CylindricalMesh from discretize.utils import mkvc -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem import numpy as np import matplotlib as mpl @@ -37,7 +37,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_file = False diff --git a/tutorials/07-fdem/plot_fwd_3_fem_3d.py b/tutorials/07-fdem/plot_fwd_3_fem_3d.py index 6dd4e7513f..f49bb96056 100644 --- a/tutorials/07-fdem/plot_fwd_3_fem_3d.py +++ b/tutorials/07-fdem/plot_fwd_3_fem_3d.py @@ -2,7 +2,7 @@ 3D Forward Simulation on a Tree Mesh ==================================== -Here we use the module *SimPEG.electromagnetics.frequency_domain* to simulate the +Here we use the module *simpeg.electromagnetics.frequency_domain* to simulate the FDEM response for an airborne survey using an OcTree mesh and a conductivity/resistivity model. To limit computational demant, we simulate airborne data at a single frequency @@ -31,9 +31,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem +from simpeg.utils import plot2Ddata +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem import os import numpy as np @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver save_file = False diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index d222fd9d62..f5e5366765 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -2,7 +2,7 @@ 1D Inversion of for a Single Sounding ===================================== -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to invert +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to invert frequency domain data and recover a 1D electrical conductivity model. In this tutorial, we focus on the following: @@ -30,9 +30,9 @@ from discretize import TensorMesh -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG.utils import mkvc, plot_1d_layer_model -from SimPEG import ( +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg.utils import mkvc, plot_1d_layer_model +from simpeg import ( maps, data, data_misfit, diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm.py b/tutorials/08-tdem/plot_fwd_1_em1dtm.py index 7700d32745..afe0db1550 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Single Sounding =========================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to predict +Here we use the module *simpeg.electromangetics.time_domain_1d* to predict the stepoff response for a single sounding over a 1D layered Earth. In this tutorial, we focus on the following: @@ -26,9 +26,9 @@ import os from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.utils import plot_1d_layer_model +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.utils import plot_1d_layer_model write_output = False plt.rcParams.update({"font.size": 16}) @@ -131,7 +131,7 @@ # The simulation requires the user define the survey, the layer thicknesses # and a mapping from the model to the conductivities of the layers. # -# When using the *SimPEG.electromagnetics.time_domain_1d* module, +# When using the *simpeg.electromagnetics.time_domain_1d* module, # predicted data are organized by source, then by receiver, then by time channel. # diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py b/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py index 2ce79e7c70..a3dc087cf3 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py @@ -2,7 +2,7 @@ 1D Forward Simulation with Chargeable and/or Magnetic Viscosity =============================================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to compare +Here we use the module *simpeg.electromangetics.time_domain_1d* to compare predicted time domain data for a single sounding when the Earth is purely conductive, chargeable and/or magnetically viscous. In this tutorial, we focus on: @@ -26,9 +26,9 @@ import numpy as np from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.electromagnetics.utils.em1d_utils import ColeCole, LogUniform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils.em1d_utils import ColeCole, LogUniform # sphinx_gallery_thumbnail_number = 3 @@ -186,7 +186,7 @@ # parameters used to define the physical properties are permanently set when # defining the simulation. # -# When using the *SimPEG.electromagnetics.time_domain_1d* module, note that +# When using the *simpeg.electromagnetics.time_domain_1d* module, note that # predicted data are organized by source, then by receiver, then by time channel. # # diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py index 66085d1804..bd307c843f 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py @@ -5,7 +5,7 @@ For time-domain electromagnetic problems, the response depends strongly on the souce waveforms. In this tutorial, we construct a set of waveforms of different types and simulate the response for a halfspace. Many types of waveforms can -be constructed within *SimPEG.electromagnetics.time_domain_1d*. These include: +be constructed within *simpeg.electromagnetics.time_domain_1d*. These include: - the unit step off waveform - a set of basic waveforms: rectangular, triangular, quarter sine, etc... @@ -26,8 +26,8 @@ mpl.rcParams.update({"font.size": 16}) -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem ##################################################################### diff --git a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py index c7bcccde1e..f3b81c0361 100644 --- a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py +++ b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py @@ -2,7 +2,7 @@ 3D Forward Simulation for Transient Response on a Cylindrical Mesh ================================================================== -Here we use the module *SimPEG.electromagnetics.time_domain* to simulate the +Here we use the module *simpeg.electromagnetics.time_domain* to simulate the transient response for borehole survey using a cylindrical mesh and a radially symmetric conductivity. For this tutorial, we focus on the following: @@ -29,8 +29,8 @@ from discretize import CylindricalMesh from discretize.utils import mkvc -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem import numpy as np import matplotlib as mpl @@ -39,7 +39,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_file = False @@ -50,7 +50,7 @@ # Defining the Waveform # --------------------- # -# Under *SimPEG.electromagnetic.time_domain.sources* +# Under *simpeg.electromagnetic.time_domain.sources* # there are a multitude of waveforms that can be defined (VTEM, Ramp-off etc...). # Here we simulate the response due to a step off waveform where the off-time # begins at t=0. Other waveforms are discuss in the OcTree simulation example. diff --git a/tutorials/08-tdem/plot_fwd_3_tem_3d.py b/tutorials/08-tdem/plot_fwd_3_tem_3d.py index 15b7609a0e..9b9aecbf79 100644 --- a/tutorials/08-tdem/plot_fwd_3_tem_3d.py +++ b/tutorials/08-tdem/plot_fwd_3_tem_3d.py @@ -2,7 +2,7 @@ 3D Forward Simulation with User-Defined Waveforms ================================================= -Here we use the module *SimPEG.electromagnetics.time_domain* to predict the +Here we use the module *simpeg.electromagnetics.time_domain* to predict the TDEM response for a trapezoidal waveform. We consider an airborne survey which uses a horizontal coplanar geometry. For this tutorial, we focus on the following: @@ -31,9 +31,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg.utils import plot2Ddata +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem import numpy as np import matplotlib as mpl @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver save_file = False @@ -68,7 +68,7 @@ # Defining the Waveform # --------------------- # -# Under *SimPEG.electromagnetic.time_domain.sources* +# Under *simpeg.electromagnetic.time_domain.sources* # there are a multitude of waveforms that can be defined (VTEM, Ramp-off etc...). # Here, we consider a trapezoidal waveform, which consists of a # linear ramp-on followed by a linear ramp-off. For each waveform, it diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index 9e189b32cb..e0dcede58a 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -2,7 +2,7 @@ 1D Inversion of Time-Domain Data for a Single Sounding ====================================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to invert +Here we use the module *simpeg.electromangetics.time_domain_1d* to invert time domain data and recover a 1D electrical conductivity model. In this tutorial, we focus on the following: @@ -30,10 +30,10 @@ from discretize import TensorMesh -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem -from SimPEG.utils import mkvc, plot_1d_layer_model -from SimPEG import ( +from simpeg.utils import mkvc, plot_1d_layer_model +from simpeg import ( maps, data, data_misfit, diff --git a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py index 8e27997da9..b783113a15 100644 --- a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py +++ b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py @@ -2,7 +2,7 @@ Response from a Homogeneous Layer for Different Waveforms ========================================================= -Here we use the module *SimPEG.electromagnetics.viscous_remanent_magnetization* +Here we use the module *simpeg.electromagnetics.viscous_remanent_magnetization* to predict the characteristic VRM response over magnetically viscous layer. We consider a small-loop, ground-based survey which uses a coincident loop geometry. For this tutorial, we focus on the following: @@ -25,7 +25,7 @@ # -------------- # -import SimPEG.electromagnetics.viscous_remanent_magnetization as vrm +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm from discretize import TensorMesh from discretize.utils import mkvc @@ -40,7 +40,7 @@ # Define Waveforms # ---------------- # -# Under *SimPEG.electromagnetic.viscous_remanent_magnetization.waveform* +# Under *simpeg.electromagnetic.viscous_remanent_magnetization.waveform* # there are a multitude of waveforms that can be defined (Step-off, square-pulse, # piecewise linear, ...). Here we define a specific waveform for each transmitter. # Each waveform is defined with a diferent set of parameters. diff --git a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py index 4c1444f426..a1a60bec98 100644 --- a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py +++ b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py @@ -2,7 +2,7 @@ Forward Simulation of VRM Response on a Tree Mesh ================================================= -Here we use the module *SimPEG.electromagnetics.viscous_remanent_magnetization* +Here we use the module *simpeg.electromagnetics.viscous_remanent_magnetization* to predict the characteristic VRM response over magnetically viscous top soil. We consider a small-loop, ground-based survey which uses a coincident loop geometry. For this tutorial, we focus on the following: @@ -26,9 +26,9 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm -from SimPEG.utils import plot2Ddata -from SimPEG import maps +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.utils import plot2Ddata +from simpeg import maps from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz diff --git a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py index 4189cfbf67..fece2a72ec 100644 --- a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py +++ b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py @@ -2,8 +2,8 @@ Forward Simulation Including Inductive Response =============================================== -Here we use the modules *SimPEG.electromagnetics.viscous_remanent_magnetization* -and *SimPEG.electromagnetics.time_domain* to simulation the transient response +Here we use the modules *simpeg.electromagnetics.viscous_remanent_magnetization* +and *simpeg.electromagnetics.time_domain* to simulation the transient response over a conductive and magnetically viscous Earth. We consider a small-loop, ground-based survey which uses a coincident loop geometry. Earth is comprised of a conductive pipe and resistive surface layer as well as a magnetically @@ -28,9 +28,9 @@ # -------------- # -import SimPEG.electromagnetics.viscous_remanent_magnetization as vrm -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG import maps +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm +import simpeg.electromagnetics.time_domain as tdem +from simpeg import maps from discretize import TensorMesh, CylindricalMesh from discretize.utils import mkvc @@ -42,7 +42,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver # sphinx_gallery_thumbnail_number = 3 diff --git a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py index 1f16d0b4cd..ed82636588 100644 --- a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py @@ -2,7 +2,7 @@ Forward Simulation for Straight Ray Tomography in 2D ==================================================== -Here we module *SimPEG.seismic.straight_ray_tomography* to predict arrival +Here we module *simpeg.seismic.straight_ray_tomography* to predict arrival time data for a synthetic velocity/slowness model. In this tutorial, we focus on the following: - How to define the survey @@ -24,9 +24,9 @@ from discretize import TensorMesh -from SimPEG import maps -from SimPEG.seismic import straight_ray_tomography as tomo -from SimPEG.utils import model_builder +from simpeg import maps +from simpeg.seismic import straight_ray_tomography as tomo +from simpeg.utils import model_builder save_file = False diff --git a/tutorials/12-seismic/plot_inv_1_tomography_2D.py b/tutorials/12-seismic/plot_inv_1_tomography_2D.py index cfa786638a..896def24fd 100644 --- a/tutorials/12-seismic/plot_inv_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_inv_1_tomography_2D.py @@ -29,7 +29,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( data, maps, regularization, @@ -41,7 +41,7 @@ utils, ) -from SimPEG.seismic import straight_ray_tomography as tomo +from simpeg.seismic import straight_ray_tomography as tomo # sphinx_gallery_thumbnail_number = 3 diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index cf271661d2..951807633a 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -36,9 +36,9 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG.potential_fields import gravity, magnetics -from SimPEG import ( +from simpeg.utils import plot2Ddata +from simpeg.potential_fields import gravity, magnetics +from simpeg import ( maps, data, data_misfit, diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index c6e170bf27..d4a1e3cc13 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -32,8 +32,8 @@ from discretize.utils import active_from_xyz import matplotlib.pyplot as plt import numpy as np -import SimPEG.potential_fields as pf -from SimPEG import ( +import simpeg.potential_fields as pf +from simpeg import ( data_misfit, directives, inverse_problem, @@ -43,7 +43,7 @@ regularization, utils, ) -from SimPEG.utils import io_utils +from simpeg.utils import io_utils # Reproducible science np.random.seed(518936) diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index 0e57205665..78582411f1 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -33,8 +33,8 @@ from discretize.utils import active_from_xyz import matplotlib.pyplot as plt import numpy as np -import SimPEG.potential_fields as pf -from SimPEG import ( +import simpeg.potential_fields as pf +from simpeg import ( data_misfit, directives, inverse_problem, @@ -44,7 +44,7 @@ regularization, utils, ) -from SimPEG.utils import io_utils +from simpeg.utils import io_utils # Reproducible science np.random.seed(518936) diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index 6282d22b7f..c035e10caf 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -32,9 +32,9 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata, mkvc -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG import ( +from simpeg.utils import plot2Ddata, mkvc +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg import ( maps, data, data_misfit, @@ -49,7 +49,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver # sphinx_gallery_thumbnail_number = 3 diff --git a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py index 06869ae165..f4694830de 100644 --- a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py @@ -20,13 +20,13 @@ from discretize import TensorMesh from pymatsolver import PardisoSolver -from SimPEG import maps -from SimPEG.utils import mkvc -import SimPEG.electromagnetics.time_domain_1d as em1d -from SimPEG.electromagnetics.utils.em1d_utils import ( +from simpeg import maps +from simpeg.utils import mkvc +import simpeg.electromagnetics.time_domain_1d as em1d +from simpeg.electromagnetics.utils.em1d_utils import ( get_vertical_discretization_time, ) -from SimPEG.electromagnetics.time_domain_1d.known_waveforms import ( +from simpeg.electromagnetics.time_domain_1d.known_waveforms import ( skytem_HM_2015, skytem_LM_2015, ) diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 6419f37d23..936b238b3c 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -20,8 +20,8 @@ from discretize import TensorMesh from pymatsolver import PardisoSolver -from SimPEG.utils import mkvc -from SimPEG import ( +from simpeg.utils import mkvc +from simpeg import ( maps, data, data_misfit, @@ -33,12 +33,12 @@ utils, ) -import SimPEG.electromagnetics.time_domain_1d as em1d -from SimPEG.electromagnetics.utils.em1d_utils import ( +import simpeg.electromagnetics.time_domain_1d as em1d +from simpeg.electromagnetics.utils.em1d_utils import ( get_2d_mesh, get_vertical_discretization_time, ) -from SimPEG.electromagnetics.time_domain_1d.known_waveforms import ( +from simpeg.electromagnetics.time_domain_1d.known_waveforms import ( skytem_HM_2015, skytem_LM_2015, ) From c2126cdd9e2a86578215df00e5e211501988ca91 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Fri, 26 Apr 2024 10:51:05 -0700 Subject: [PATCH 406/455] Update copyright year in __init__.py (#1436) Update the copyright year in the init --- simpeg/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg/__init__.py b/simpeg/__init__.py index 36996feac9..f46c2b0e0b 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -166,7 +166,7 @@ __author__ = "SimPEG Team" __license__ = "MIT" -__copyright__ = "2013 - 2020, SimPEG Team, https://simpeg.xyz" +__copyright__ = "2013 - 2024, SimPEG Team, https://simpeg.xyz" # Version From 0f90d5513c0a3194d9d04e9af969e8e10747b225 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 Apr 2024 09:25:47 -0700 Subject: [PATCH 407/455] Replace SimPEG for simpeg across docstrings (#1438) Replace the usage of SimPEG in favor or simpeg in docstrings while specifying valid data types. --- simpeg/directives/directives.py | 18 +++--- .../frequency_domain/fields.py | 64 +++++++++---------- .../frequency_domain/simulation.py | 4 +- .../frequency_domain/simulation_1d.py | 2 +- .../frequency_domain/sources.py | 12 ++-- .../electromagnetics/natural_source/fields.py | 2 +- .../natural_source/receivers.py | 8 +-- .../natural_source/simulation_1d.py | 2 +- .../natural_source/utils/data_utils.py | 8 +-- .../natural_source/utils/data_viewer.py | 2 +- .../static/induced_polarization/survey.py | 4 +- .../static/resistivity/receivers.py | 4 +- .../static/resistivity/simulation.py | 2 +- .../static/resistivity/simulation_1d.py | 2 +- .../static/resistivity/simulation_2d.py | 2 +- .../static/resistivity/sources.py | 10 +-- .../receivers.py | 4 +- .../simulation.py | 2 +- .../spectral_induced_polarization/sources.py | 8 +-- .../spectral_induced_polarization/survey.py | 4 +- .../static/utils/static_utils.py | 16 ++--- .../time_domain/simulation_1d.py | 2 +- .../electromagnetics/time_domain/sources.py | 6 +- .../viscous_remanent_magnetization/sources.py | 14 ++-- simpeg/meta/dask_sim.py | 2 +- simpeg/meta/multiprocessing.py | 6 +- simpeg/meta/simulation.py | 8 +-- simpeg/potential_fields/gravity/simulation.py | 2 +- simpeg/potential_fields/gravity/survey.py | 2 +- simpeg/regularization/correspondence.py | 2 +- simpeg/regularization/cross_gradient.py | 2 +- simpeg/regularization/jtv.py | 2 +- simpeg/regularization/pgi.py | 10 +-- .../io_utils/io_utils_electromagnetics.py | 8 +-- 34 files changed, 123 insertions(+), 123 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 21659338f1..156f1acb33 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -97,7 +97,7 @@ def inversion(self): Returns ------- - SimPEG.inversion.BaseInversion + simpeg.inversion.BaseInversion The inversion associated with the directive. """ if not hasattr(self, "_inversion"): @@ -121,7 +121,7 @@ def invProb(self): Returns ------- - SimPEG.inverse_problem.BaseInvProblem + simpeg.inverse_problem.BaseInvProblem The inverse problem associated with the directive. """ return self.inversion.invProb @@ -132,7 +132,7 @@ def opt(self): Returns ------- - SimPEG.optimization.Minimize + simpeg.optimization.Minimize Optimization algorithm associated with the directive. """ return self.invProb.opt @@ -143,7 +143,7 @@ def reg(self): Returns ------- - SimPEG.regularization.BaseRegularization + simpeg.regularization.BaseRegularization The regularization associated with the directive. """ if getattr(self, "_reg", None) is None: @@ -167,7 +167,7 @@ def dmisfit(self): Returns ------- - SimPEG.data_misfit.BaseDataMisfit + simpeg.data_misfit.BaseDataMisfit The data misfit associated with the directive. """ if getattr(self, "_dmisfit", None) is None: @@ -236,7 +236,7 @@ def validate(self, directiveList=None): Parameters ---------- - directive_list : SimPEG.directives.DirectiveList + directive_list : simpeg.directives.DirectiveList List of directives used in the inversion. Returns @@ -256,9 +256,9 @@ class DirectiveList(object): Parameters ---------- - directives : list of SimPEG.directives.InversionDirective + directives : list of simpeg.directives.InversionDirective List of directives. - inversion : SimPEG.inversion.BaseInversion + inversion : simpeg.inversion.BaseInversion The inversion associated with the directives list. debug : bool Whether or not to print debugging information. @@ -298,7 +298,7 @@ def inversion(self): Returns ------- - SimPEG.inversion.BaseInversion + simpeg.inversion.BaseInversion The inversion associated with the directives list. """ return getattr(self, "_inversion", None) diff --git a/simpeg/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py index ed950b91f7..ecf0023c55 100644 --- a/simpeg/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -375,7 +375,7 @@ def _eDeriv_u(self, src, v, adjoint=False): Partial derivative of the total electric field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -392,7 +392,7 @@ def _eDeriv_m(self, src, v, adjoint=False): the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: simpeg.utils.Zero @@ -455,7 +455,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -473,7 +473,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -505,7 +505,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the current density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -524,7 +524,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -563,7 +563,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -588,7 +588,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -702,7 +702,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total magnetic flux density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -719,7 +719,7 @@ def _bDeriv_m(self, src, v, adjoint=False): on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: simpeg.utils.Zero @@ -779,7 +779,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -795,7 +795,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -852,7 +852,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the current density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -876,7 +876,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -902,7 +902,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the magnetic field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -923,7 +923,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1044,7 +1044,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total current density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1061,7 +1061,7 @@ def _jDeriv_m(self, src, v, adjoint=False): the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: simpeg.utils.Zero @@ -1123,7 +1123,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1149,7 +1149,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1228,7 +1228,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1243,7 +1243,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1276,7 +1276,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1302,7 +1302,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1421,7 +1421,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total magnetic field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1438,7 +1438,7 @@ def _hDeriv_m(self, src, v, adjoint=False): on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: simpeg.utils.Zero @@ -1487,7 +1487,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the current density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1504,7 +1504,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1533,7 +1533,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1548,7 +1548,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1586,7 +1586,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1609,7 +1609,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index c68b8f3647..19619a8f89 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -882,7 +882,7 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): Derivative of the right hand side with respect to the model :param float freq: frequency - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1043,7 +1043,7 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): Derivative of the right hand side with respect to the model :param float freq: frequency - :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source + :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/simpeg/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py index fc9200b146..7d880dd09c 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -28,7 +28,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey + simpeg.electromagnetics.frequency_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index dc88bc5922..c92ab26b3c 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -653,7 +653,7 @@ class MagDipole_Bfield(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -734,7 +734,7 @@ class CircularLoop(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -1207,7 +1207,7 @@ class LineCurrent(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx List of FDEM receivers frequency : float Source frequency @@ -1295,7 +1295,7 @@ def Mejs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns @@ -1314,7 +1314,7 @@ def Mfjs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns @@ -1333,7 +1333,7 @@ def getRHSdc(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns diff --git a/simpeg/electromagnetics/natural_source/fields.py b/simpeg/electromagnetics/natural_source/fields.py index cb43b4296e..2493c7f0bd 100644 --- a/simpeg/electromagnetics/natural_source/fields.py +++ b/simpeg/electromagnetics/natural_source/fields.py @@ -325,7 +325,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index 1bc5bda13d..9e76a2fb4e 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -575,11 +575,11 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh mesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source return_complex : bool (optional) Flag for return the complex evaluation @@ -599,11 +599,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - str : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source du_dm_v : None, Supply pre-computed derivative? diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index a76e162728..8ab8992ec5 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -71,7 +71,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey + simpeg.electromagnetics.frequency_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") diff --git a/simpeg/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py index 6c1783428f..eb1577be2b 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -21,7 +21,7 @@ def rotate_data(NSEMdata, rot_angle): Function that rotates clockwise by rotation angle (- negative for a counter-clockwise rotation) - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param float rot_angle: Rotation angel in degrees, positive for clockwise rotation """ recData = NSEMdata.toRecArray("Complex") @@ -56,7 +56,7 @@ def extract_data_info(NSEMdata): Useful when assigning uncertainties to data based on frequencies and receiver types. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process """ dL, freqL, rxTL = [], [], [] @@ -78,7 +78,7 @@ def resample_data(NSEMdata, locs="All", freqs="All", rxs="All", verbose=False): (uses the numerator location as a reference). Also gives the option of selecting frequencies and receiver. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param locs: receiver locations to use (default is 'All' locations) :type locs: numpy.ndarray, optional @@ -217,7 +217,7 @@ def convert3Dto1Dobject(NSEMdata, rxType3D="yx"): Function that converts a 3D NSEMdata of a list of 1D NSEMdata objects for running 1D inversions for. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param rxType3D: component of the NSEMdata to use. Can be 'xy', 'yx' or 'det' diff --git a/simpeg/electromagnetics/natural_source/utils/data_viewer.py b/simpeg/electromagnetics/natural_source/utils/data_viewer.py index 0d04481a16..1e8270fcaa 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_viewer.py +++ b/simpeg/electromagnetics/natural_source/utils/data_viewer.py @@ -14,7 +14,7 @@ class NSEM_data_viewer: Generates a clickble location map of the data, plotting data curves in a separate window. - :param SimPEG.electromagnetics.natural_source.Data data: Data object, needs to have assigned + :param simpeg.electromagnetics.natural_source.Data data: Data object, needs to have assigned relative_error and noise_floor :param data_dict: A dictionary of other NSEM Data objects diff --git a/simpeg/electromagnetics/static/induced_polarization/survey.py b/simpeg/electromagnetics/static/induced_polarization/survey.py index 742df99710..54afd3e080 100644 --- a/simpeg/electromagnetics/static/induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/induced_polarization/survey.py @@ -7,14 +7,14 @@ def from_dc_to_ip_survey(dc_survey, dim="2.5D"): Parameters ---------- - dc_survey : SimPEG.electromagnetics.static.resistivity.survey.Survey + dc_survey : simpeg.electromagnetics.static.resistivity.survey.Survey DC survey object dim : {'2.5D', '1D', '3D'} Dimension of the surface. Returns ------- - SimPEG.electromagnetics.static.induced_polarization.survey.Survey + simpeg.electromagnetics.static.induced_polarization.survey.Survey An IP survey object """ source_list = dc_survey.source_list diff --git a/simpeg/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py index df3009a4dc..3e54d85d96 100644 --- a/simpeg/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -158,7 +158,7 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.static.resistivity.sources.BaseSrc + src : simpeg.electromagnetics.static.resistivity.sources.BaseSrc A DC/IP source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved @@ -198,7 +198,7 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.static.resistivity.sources.BaseSrc + src : simpeg.electromagnetics.static.resistivity.sources.BaseSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index 0c07dc0b24..2a78b6a17b 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -49,7 +49,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/simpeg/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py index 5482025666..f2528d9a6f 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -165,7 +165,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey.") diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 163ef59774..239d51536c 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -134,7 +134,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/simpeg/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py index cb2ece9736..dd55089689 100644 --- a/simpeg/electromagnetics/static/resistivity/sources.py +++ b/simpeg/electromagnetics/static/resistivity/sources.py @@ -8,7 +8,7 @@ class BaseSrc(survey.BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (n_source, dim) numpy.ndarray Source locations @@ -66,7 +66,7 @@ def eval(self, sim): # noqa: A003 Parameters ---------- - sim : SimPEG.base.BaseElectricalPDESimulation + sim : simpeg.base.BaseElectricalPDESimulation The static electromagnetic simulation Returns @@ -94,7 +94,7 @@ def evalDeriv(self, sim): Parameters ---------- - sim : SimPEG.base.BaseElectricalPDESimulation + sim : simpeg.base.BaseElectricalPDESimulation The static electromagnetic simulation Returns @@ -137,7 +137,7 @@ class Dipole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location_a : (n_source, dim) numpy.array_like A electrode locations; remember to set 'location_b' keyword argument to define N electrode locations. @@ -224,7 +224,7 @@ class Pole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (n_source, dim) numpy.ndarray Electrode locations diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py index 3865349fab..3fc73b0f86 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py @@ -144,7 +144,7 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.static.spectral_induced_polarization.sources.BaseRx + src : simpeg.electromagnetics.static.spectral_induced_polarization.sources.BaseRx A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved @@ -172,7 +172,7 @@ def evalDeriv(self, src, mesh, f, v, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.static.spectral_induced_polarization.sources.BaseRx + src : simpeg.electromagnetics.static.spectral_induced_polarization.sources.BaseRx A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 79627480a1..87e5638875 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -64,7 +64,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.spectral_induced_polarization.survey.Survey + simpeg.electromagnetics.static.spectral_induced_polarization.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py index 5553833128..2bb93a56bf 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py @@ -10,7 +10,7 @@ class BaseSrc(survey.BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (dim) numpy.ndarray Source location @@ -95,7 +95,7 @@ class Dipole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx A list of spectral IP receivers location_a : (dim) numpy.ndarray A electrode location; remember to set 'location_b' keyword argument to define N electrode locations. @@ -238,7 +238,7 @@ class Pole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx list of spectral IP receivers location : (dim) array_like Electrode location @@ -254,7 +254,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - sim : SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation + sim : simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation A DC/IP simulation Returns diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 4f3dbd1d27..05f5a0d460 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -109,14 +109,14 @@ def from_dc_to_sip_survey(survey_dc, times): Parameters ---------- - dc_survey : SimPEG.electromagnetics.static.resistivity.survey.Survey + dc_survey : simpeg.electromagnetics.static.resistivity.survey.Survey DC survey object times : numpy.ndarray Time channels Returns ------- - SimPEG.electromagnetics.static.spectral_induced_polarization.survey.Survey + simpeg.electromagnetics.static.spectral_induced_polarization.survey.Survey An SIP survey object """ source_list = survey_dc.source_list diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index d52892c2aa..c863fb9c99 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -178,7 +178,7 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.Survey A DC or IP survey wenner_tolerance : float, default=0.1 If the center location for a source and receiver pair are within wenner_tolerance, @@ -269,7 +269,7 @@ def geometric_factor(survey_object, space_type="half space", **kwargs): Parameters ---------- - survey_object : SimPEG.electromagnetics.static.resistivity.Survey + survey_object : simpeg.electromagnetics.static.resistivity.Survey A DC (or IP) survey object space_type : {'half space', 'whole space'} Compute geometric factor for a halfspace or wholespace. @@ -312,7 +312,7 @@ def apparent_resistivity_from_voltage( Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.Survey A DC survey volts : (nD) numpy.ndarray Normalized voltage measurements [V/A] @@ -352,7 +352,7 @@ def convert_survey_3d_to_2d_lines( Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.Survey A DC (or IP) survey lineID : (n_data) numpy.ndarray Defines the corresponding line ID for each datum @@ -364,7 +364,7 @@ def convert_survey_3d_to_2d_lines( Returns ------- - survey_list : list of SimPEG.electromagnetics.static.resistivity.Survey + survey_list : list of simpeg.electromagnetics.static.resistivity.Survey A list of 2D survey objects out_indices_list : list of numpy.ndarray A list of indexing arrays that map from the original 3D data to each 2D @@ -504,7 +504,7 @@ def plot_pseudosection( Parameters ---------- - data : simpeg.electromagnetics.static.survey.Survey or SimPEG.data.Data + data : simpeg.electromagnetics.static.survey.Survey or simpeg.data.Data A DC or IP survey object defining a 2D survey line, or a Data object containing that same type of survey object. dobs : numpy.ndarray (ndata,) or None @@ -1096,7 +1096,7 @@ def generate_dcip_survey(endl, survey_type, a, b, n, dim=3, **kwargs): Returns ------- - SimPEG.electromagnetics.static.resistivity.Survey + simpeg.electromagnetics.static.resistivity.Survey A DC survey object """ @@ -1285,7 +1285,7 @@ def generate_dcip_sources_line( Returns ------- - SimPEG.electromagnetics.static.resistivity.Survey + simpeg.electromagnetics.static.resistivity.Survey A DC survey object """ diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index afac06ae5b..3343ec3fba 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -36,7 +36,7 @@ def survey(self): """The survey for the simulation Returns ------- - SimPEG.electromagnetics.time_domain.survey.Survey + simpeg.electromagnetics.time_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index a3b63b1172..8fd82da548 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -1719,7 +1719,7 @@ def Mfjs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns @@ -1738,7 +1738,7 @@ def getRHSdc(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns @@ -1762,7 +1762,7 @@ def phiInitial(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An electromagnetic simulation Returns diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py index 5ddd369e98..b93f206a53 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py @@ -20,7 +20,7 @@ class BaseSrcVRM(BaseSrc): A list of VRM receivers location : (3) array_like Source location - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform A VRM waveform """ @@ -68,13 +68,13 @@ class MagDipole(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point VRM receivers location : (3) array_like source location moment : (3) array_like dipole moment (mx, my, mz) - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ @@ -189,7 +189,7 @@ class CircLoop(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point VRM receivers location : (3) array_like source location @@ -199,7 +199,7 @@ class CircLoop(BaseSrcVRM): Circular loop normal azimuth and declination Imax : float Maximum current amplitude - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ @@ -379,14 +379,14 @@ class LineCurrent(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx List of TDEM receivers location : (n, 3) numpy.ndarray Array defining the node locations for the wire path. For inductive sources, you must close the loop. Imax : float Maximum current amplitude - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ diff --git a/simpeg/meta/dask_sim.py b/simpeg/meta/dask_sim.py index e52db04b56..bddf091920 100644 --- a/simpeg/meta/dask_sim.py +++ b/simpeg/meta/dask_sim.py @@ -586,7 +586,7 @@ class DaskRepeatedSimulation(DaskMetaSimulation): Parameters ---------- - simulation : SimPEG.simulation.BaseSimulation or dask.distributed.Future + simulation : simpeg.simulation.BaseSimulation or dask.distributed.Future The simulation to use repeatedly with different mappings. mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future The list of different mappings to use (or futures that each return a mapping). diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index 56dbb2df65..23e025688b 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -201,10 +201,10 @@ class MultiprocessingMetaSimulation(MetaSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + simulations : (n_sim) list of simpeg.simulation.BaseSimulation The list of unique simulations that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The map for every simulation. Every map should accept the same length model, and output a model appropriate for its paired simulation. @@ -435,7 +435,7 @@ class MultiprocessingRepeatedSimulation( Parameters ---------- - simulation : SimPEG.simulation.BaseSimulation + simulation : simpeg.simulation.BaseSimulation The simulation to use repeatedly with different mappings. mappings : (n_sim) list of simpeg.maps.IdentityMap The list of different mappings to use. diff --git a/simpeg/meta/simulation.py b/simpeg/meta/simulation.py index 9fd90dcccb..ae9846475a 100644 --- a/simpeg/meta/simulation.py +++ b/simpeg/meta/simulation.py @@ -26,10 +26,10 @@ class MetaSimulation(BaseSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + simulations : (n_sim) list of simpeg.simulation.BaseSimulation The list of unique simulations that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The map for every simulation. Every map should accept the same length model, and output a model appropriate for its paired simulation. @@ -432,7 +432,7 @@ class RepeatedSimulation(MetaSimulation): Parameters ---------- - simulation : SimPEG.simulation.BaseSimulation + simulation : simpeg.simulation.BaseSimulation The simulation to use repeatedly with different mappings. mappings : (n_sim) list of simpeg.maps.IdentityMap The list of different mappings to use. @@ -467,7 +467,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseSimulation + simpeg.simulation.BaseSimulation """ return self._simulation diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index fdc2b68d82..311b19dfc5 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -81,7 +81,7 @@ class Simulation3DIntegral(BasePFSimulation): ---------- mesh : discretize.TreeMesh or discretize.TensorMesh Mesh use to run the gravity simulation. - survey : SimPEG.potential_fields.gravity.Survey + survey : simpeg.potential_fields.gravity.Survey Gravity survey with information of the receivers. ind_active : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. diff --git a/simpeg/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py index 7b3b01cf60..2808e2489a 100644 --- a/simpeg/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -8,7 +8,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.gravity.sources.SourceField + source_field : simpeg.potential_fields.gravity.sources.SourceField A source object that defines receivers locations for gravity. """ diff --git a/simpeg/regularization/correspondence.py b/simpeg/regularization/correspondence.py index fe9060dc5b..98e51443ae 100644 --- a/simpeg/regularization/correspondence.py +++ b/simpeg/regularization/correspondence.py @@ -22,7 +22,7 @@ class LinearCorrespondence(BaseSimilarityMeasure): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - wire_map : SimPEG.maps.Wires + wire_map : simpeg.maps.Wires Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh`` to the entire model. coefficients : None, (3) numpy.ndarray of float diff --git a/simpeg/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py index e245233a1a..bf9353d652 100644 --- a/simpeg/regularization/cross_gradient.py +++ b/simpeg/regularization/cross_gradient.py @@ -29,7 +29,7 @@ class CrossGradient(BaseSimilarityMeasure): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - wire_map : SimPEG.maps.Wires + wire_map : simpeg.maps.Wires Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh`` to the entire model. reference_model : None, (n_param, ) numpy.ndarray diff --git a/simpeg/regularization/jtv.py b/simpeg/regularization/jtv.py index ce37a309db..0014071a30 100644 --- a/simpeg/regularization/jtv.py +++ b/simpeg/regularization/jtv.py @@ -27,7 +27,7 @@ class JointTotalVariation(BaseSimilarityMeasure): active_cells : None, (n_cells, ) numpy.ndarray of bool Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. - wire_map : SimPEG.maps.Wires + wire_map : simpeg.maps.Wires Wire map connecting physical properties defined on active cells of the :class:`RegularizationMesh`` to the entire model. reference_model : None, (n_param, ) numpy.ndarray diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index 24be874eff..08a8f4ddef 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -51,7 +51,7 @@ class PGIsmallness(Smallness): Parameters ---------- - gmmref : SimPEG.utils.WeightedGaussianMixture + gmmref : simpeg.utils.WeightedGaussianMixture Reference Gaussian mixture model. gmm : None, simpeg.utils.WeightedGaussianMixture Set the Gaussian mixture model used to constrain the recovered physical property model. @@ -267,7 +267,7 @@ def gmm(self): Returns ------- - SimPEG.utils.WeightedGaussianMixture + simpeg.utils.WeightedGaussianMixture Gaussian mixture model used to constrain the recovered physical property model. """ if getattr(self, "_gmm", None) is None: @@ -373,7 +373,7 @@ def wiresmap(self): Returns ------- - SimPEG.maps.Wires + simpeg.maps.Wires Mapping from the model to the model parameters of each type. """ if getattr(self, "_wiresmap", None) is None: @@ -975,7 +975,7 @@ class PGI(ComboObjectiveFunction): mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh Mesh on which the regularization is discretized. Implemented for `tensor`, `QuadTree` or `Octree` meshes. - gmmref : SimPEG.utils.WeightedGaussianMixture + gmmref : simpeg.utils.WeightedGaussianMixture Reference Gaussian mixture model. gmm : None, simpeg.utils.WeightedGaussianMixture Set the Gaussian mixture model used to constrain the recovered physical property model. @@ -1301,7 +1301,7 @@ def wiresmap(self): Returns ------- - SimPEG.maps.Wires + simpeg.maps.Wires Mapping from the model to the model parameters of each type. """ if getattr(self, "_wiresmap", None) is None: diff --git a/simpeg/utils/io_utils/io_utils_electromagnetics.py b/simpeg/utils/io_utils/io_utils_electromagnetics.py index 6d44676987..e0306a17ef 100644 --- a/simpeg/utils/io_utils/io_utils_electromagnetics.py +++ b/simpeg/utils/io_utils/io_utils_electromagnetics.py @@ -207,7 +207,7 @@ def read_dcip2d_ubc(file_name, data_type, format_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: @@ -435,7 +435,7 @@ def read_dcip3d_ubc(file_name, data_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: @@ -595,7 +595,7 @@ def read_dcipoctree_ubc(file_name, data_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: @@ -992,7 +992,7 @@ def write_dcip_xyz( ---------- file_name : str Path to the file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data simpeg.data.Data object. The `survey` attribute of this data object must be an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` From a22fb582b3a43dfd9dc621f65bd6a9e17a407d59 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 Apr 2024 12:44:51 -0700 Subject: [PATCH 408/455] Lowercase simpeg for generating coverage on Azure (#1443) Lowercase the `simpeg` package when running tests and generating coverage in Azure pipelines. This way we are correctly generating the coverage report for the `simpeg` package. --- .azure-pipelines/matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index 2b96f56561..cc3fa7c170 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -47,7 +47,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test export KMP_WARNINGS=0 - pytest ${{ test }} -v --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning + pytest ${{ test }} -v --cov-config=.coveragerc --cov=simpeg --cov-report=xml --cov-report=html -W ignore::DeprecationWarning displayName: 'Testing ${{ test }}' - task: PublishPipelineArtifact@1 From aa0a258581c5e0e8a15d1513461432ddd622f9c0 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 1 May 2024 15:27:15 -0700 Subject: [PATCH 409/455] Rename spontaneous potential to self potential (#1422) Deprecate the `spontaneous_potential` module and replace it by the new `self_potential`. Raise warning when importing the deprecated module. Add tests that check the warning and if all modules in `self_potential` are made available also in the deprecated module. Update API reference to apply the replacement. Closes #1408 --------- Co-authored-by: Santiago Soler Co-authored-by: Joseph Capriotti --- docs/content/api/SimPEG.electromagnetics.rst | 2 +- ...electromagnetics.static.self_potential.rst | 1 + simpeg/electromagnetics/static/__init__.py | 1 + .../static/self_potential/__init__.py | 52 +++++++++++++++++++ .../simulation.py | 6 +-- .../sources.py | 0 .../static/spontaneous_potential/__init__.py | 21 +++++++- tests/em/static/test_SPjvecjtvecadj.py | 28 +++++++++- 8 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 docs/content/api/SimPEG.electromagnetics.static.self_potential.rst create mode 100644 simpeg/electromagnetics/static/self_potential/__init__.py rename simpeg/electromagnetics/static/{spontaneous_potential => self_potential}/simulation.py (98%) rename simpeg/electromagnetics/static/{spontaneous_potential => self_potential}/sources.py (100%) diff --git a/docs/content/api/SimPEG.electromagnetics.rst b/docs/content/api/SimPEG.electromagnetics.rst index a66c578f7c..b465abd441 100644 --- a/docs/content/api/SimPEG.electromagnetics.rst +++ b/docs/content/api/SimPEG.electromagnetics.rst @@ -10,7 +10,7 @@ Things about electromagnetics SimPEG.electromagnetics.static.induced_polarization SimPEG.electromagnetics.static.resistivity SimPEG.electromagnetics.static.spectral_induced_polarization - SimPEG.electromagnetics.static.spontaneous_potential + SimPEG.electromagnetics.static.self_potential SimPEG.electromagnetics.frequency_domain SimPEG.electromagnetics.natural_source SimPEG.electromagnetics.time_domain diff --git a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst new file mode 100644 index 0000000000..ae8d5f2859 --- /dev/null +++ b/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst @@ -0,0 +1 @@ +.. automodule:: SimPEG.electromagnetics.static.self_potential diff --git a/simpeg/electromagnetics/static/__init__.py b/simpeg/electromagnetics/static/__init__.py index efdaece043..4db8ce3cae 100644 --- a/simpeg/electromagnetics/static/__init__.py +++ b/simpeg/electromagnetics/static/__init__.py @@ -1,4 +1,5 @@ from . import resistivity from . import induced_polarization +from . import self_potential from . import spectral_induced_polarization from . import utils diff --git a/simpeg/electromagnetics/static/self_potential/__init__.py b/simpeg/electromagnetics/static/self_potential/__init__.py new file mode 100644 index 0000000000..86b22da6fb --- /dev/null +++ b/simpeg/electromagnetics/static/self_potential/__init__.py @@ -0,0 +1,52 @@ +""" +============================================================================================ +Self Potential (:mod:`simpeg.electromagnetics.static.self_potential`) +============================================================================================ +.. currentmodule:: simpeg.electromagnetics.static.self_potential + + +Simulations +=========== +.. autosummary:: + :toctree: generated/ + + Simulation3DCellCentered + +Receivers +========= +This module makes use of the receivers in :mod:`simpeg.electromagnetics.static.resistivity` + +Sources +======= +.. autosummary:: + :toctree: generated/ + + sources.StreamingCurrents + +Surveys +======= +.. autosummary:: + :toctree: generated/ + + Survey + +Maps +==== +The self potential simulation provides two specialized maps to extend to inversions +with different types of model sources. + +.. autosummary:: + :toctree: generated/ + + CurrentDensityMap + HydraulicHeadMap + +""" + +from .simulation import ( + Simulation3DCellCentered, + Survey, + CurrentDensityMap, + HydraulicHeadMap, +) +from . import sources diff --git a/simpeg/electromagnetics/static/spontaneous_potential/simulation.py b/simpeg/electromagnetics/static/self_potential/simulation.py similarity index 98% rename from simpeg/electromagnetics/static/spontaneous_potential/simulation.py rename to simpeg/electromagnetics/static/self_potential/simulation.py index 007fd282fe..b97d6c21ad 100644 --- a/simpeg/electromagnetics/static/spontaneous_potential/simulation.py +++ b/simpeg/electromagnetics/static/self_potential/simulation.py @@ -8,12 +8,12 @@ class Simulation3DCellCentered(dc.Simulation3DCellCentered): - r"""A Spontaneous potential simulation. + r"""A self potential simulation. Parameters ---------- mesh : discretize.base.BaseMesh - survey : spontaneous_potential.Survey + survey : simpeg.electromagnetics.static.self_potential.Survey sigma, rho : float or array_like The conductivity/resistivity model of the subsurface. q : float, array_like, optional @@ -27,7 +27,7 @@ class Simulation3DCellCentered(dc.Simulation3DCellCentered): Notes ----- - The charge density accumulation rate, :math:`q`, is related to the spontaneous + The charge density accumulation rate, :math:`q`, is related to the self electric potential, :math:`\phi`, with the same PDE, that relates current sources to potential in the resistivity case. diff --git a/simpeg/electromagnetics/static/spontaneous_potential/sources.py b/simpeg/electromagnetics/static/self_potential/sources.py similarity index 100% rename from simpeg/electromagnetics/static/spontaneous_potential/sources.py rename to simpeg/electromagnetics/static/self_potential/sources.py diff --git a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py index 25d32706f7..86323379dd 100644 --- a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py @@ -4,6 +4,10 @@ ============================================================================================ .. currentmodule:: simpeg.electromagnetics.static.spontaneous_potential +.. admonition:: important + + This module will be deprecated in favour of ``simpeg.electromagnetics.static.self_potential`` + Simulations =========== @@ -43,10 +47,23 @@ """ -from .simulation import ( +import warnings + +warnings.warn( + ( + "The 'spontaneous_potential' module has been renamed to 'self_potential'. " + "Please use the 'self_potential' module instead. " + "The 'spontaneous_potential' module will be removed in SimPEG 0.23." + ), + FutureWarning, + stacklevel=2, +) + +from ..self_potential.simulation import ( Simulation3DCellCentered, Survey, CurrentDensityMap, HydraulicHeadMap, ) -from . import sources +from ..self_potential import sources +from ..self_potential import simulation diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index d6b585ee50..8d806a3de3 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -1,6 +1,6 @@ import pytest import numpy as np -import simpeg.electromagnetics.static.spontaneous_potential as sp +import simpeg.electromagnetics.static.self_potential as sp import simpeg.electromagnetics.static.resistivity as dc import discretize from simpeg import utils @@ -117,3 +117,29 @@ def test_clears(): sim.qMap = maps.ExpMap() assert sim.deleteTheseOnModelUpdate == ["_Jmatrix", "_gtgdiag"] assert sim.clean_on_model_update == [] + + +def test_deprecations(): + """ + Test warning after importing deprecated `spontaneous_potential` module + """ + msg = ( + "The 'spontaneous_potential' module has been renamed to 'self_potential'. " + "Please use the 'self_potential' module instead. " + "The 'spontaneous_potential' module will be removed in SimPEG 0.23." + ) + with pytest.warns(FutureWarning, match=msg): + import simpeg.electromagnetics.static.spontaneous_potential # noqa: F401 + + +def test_imported_objects_on_deprecated_module(): + """ + Test if the new `self_potential` module and the deprecated `spontaneous + potential` have the same members. + """ + import simpeg.electromagnetics.static.spontaneous_potential as spontaneous + + members_self = set([m for m in dir(sp) if not m.startswith("_")]) + members_spontaneous = set([m for m in dir(spontaneous) if not m.startswith("_")]) + difference = members_self - members_spontaneous + assert not difference From fe444e2d714ab1a3f1541575a9f8c4ad1e74894b Mon Sep 17 00:00:00 2001 From: Laura Kehrl <156835230+kehrl-kobold@users.noreply.github.com> Date: Mon, 6 May 2024 11:33:16 -0700 Subject: [PATCH 410/455] Fix distance calculation in `convert_survey_3d_to_2d_lines` (#1390) Fix a bug in the distance calculation in `convert_survey_3d_to_2d_lines`. Distance is calculated along the survey line from the first point, `r0`. Currently, `r0[0]`, which is just the `x` component, is being subtracted from both the `x` and `y` components for all points to calculate the distance. Instead, `r0` should be subtracted. Update tests so they fail without the bugfix. --- .../static/utils/static_utils.py | 8 +-- tests/em/static/test_DC_Utils.py | 49 ++++++++++++------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index c863fb9c99..30edce9209 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -405,19 +405,19 @@ def convert_survey_3d_to_2d_lines( # Along line positions and elevation for electrodes on current line # in terms of position elevation a_locs_s = np.c_[ - np.dot(ab_locs_all[lineID_index, 0:2] - r0[0], uvec), + np.dot(ab_locs_all[lineID_index, 0:2] - r0, uvec), ab_locs_all[lineID_index, 2], ] b_locs_s = np.c_[ - np.dot(ab_locs_all[lineID_index, 3:5] - r0[0], uvec), + np.dot(ab_locs_all[lineID_index, 3:5] - r0, uvec), ab_locs_all[lineID_index, -1], ] m_locs_s = np.c_[ - np.dot(mn_locs_all[lineID_index, 0:2] - r0[0], uvec), + np.dot(mn_locs_all[lineID_index, 0:2] - r0, uvec), mn_locs_all[lineID_index, 2], ] n_locs_s = np.c_[ - np.dot(mn_locs_all[lineID_index, 3:5] - r0[0], uvec), + np.dot(mn_locs_all[lineID_index, 3:5] - r0, uvec), mn_locs_all[lineID_index, -1], ] diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index e8b3e133a5..bc872fcc85 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -225,27 +225,36 @@ def setUp(self): survey_type = ["dipole-dipole", "pole-pole", "pole-dipole", "dipole-pole"] data_type = "volt" dimension_type = "3D" - end_locations = np.r_[-1000.0, 1000.0, 0.0, 0.0] + end_locations = [ + np.r_[-1000.0, 1000.0, 0.0, 0.0], + np.r_[0.0, 0.0, -1000.0, 1000.0], + ] station_separation = 200.0 num_rx_per_src = 5 # The source lists for each line can be appended to create the source # list for the whole survey. source_list = [] - for ii in range(0, len(survey_type)): - source_list += utils.generate_dcip_sources_line( - survey_type[ii], - data_type, - dimension_type, - end_locations, - 0.0, - num_rx_per_src, - station_separation, - ) + lineID = [] + for end_location in end_locations: + for survey_type_i in survey_type: + source_list += utils.generate_dcip_sources_line( + survey_type_i, + data_type, + dimension_type, + end_location, + 0.0, + num_rx_per_src, + station_separation, + ) # Define the survey self.survey = dc.survey.Survey(source_list) + lineID = np.ones(len(self.survey.locations_a)) + lineID[int(len(self.survey.locations_a) / 2) :] = 2 + self.lineID = lineID + def test_generate_survey_from_abmn_locations(self): survey_new, sorting_index = utils.generate_survey_from_abmn_locations( locations_a=self.survey.locations_a, @@ -281,7 +290,7 @@ def test_get_source_locations(self): is_rx = np.array(is_rx, dtype=float) _, idx = np.unique( - np.c_[self.survey.locations_a, self.survey.locations_b, is_rx], + np.c_[self.survey.locations_a, self.survey.locations_b, is_rx, self.lineID], axis=0, return_index=True, ) @@ -299,13 +308,14 @@ def test_get_source_locations(self): self.assertTrue(passed) def test_convert_to_2d(self): - # Only 1 line of 3D data along x direction starting from (-1000,0,0) - lineID = np.ones(self.survey.nD, dtype=int) - survey_2d, IND = utils.convert_survey_3d_to_2d_lines( - self.survey, lineID, data_type="volt", output_indexing=True + # 3D survey has two lines of data, one E-W and the other N-S. + survey_2d_list, IND = utils.convert_survey_3d_to_2d_lines( + self.survey, self.lineID, data_type="volt", output_indexing=True ) + + # First, check that coordinates remain the same even after the transformation for the first line IND = IND[0] - survey_2d = survey_2d[0] + survey_2d = survey_2d_list[0] ds = np.c_[-1000.0, 0.0, 0.0] @@ -326,9 +336,14 @@ def test_convert_to_2d(self): survey_2d.locations_n, ] + # Coordinates should be roughly the same passed = np.allclose(loc3d[:, 0::2], loc2d) self.assertTrue(passed) + # Check that the first x-coordinate for electrode A is zero for both surveys + for survey in survey_2d_list: + self.assertEqual(survey.locations_a[0, 0], 0) + if __name__ == "__main__": unittest.main() From c4b2750f5590aeb11f27d7f15cee904ab1cde03c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 May 2024 11:33:30 -0700 Subject: [PATCH 411/455] Replace SimPEG for simpeg in API reference (#1446) Rename rst files in `docs/content/api` and replace `SimPEG` for `simpeg` in `docs/content/api/index.rst`. Fixes #1437 --- docs/content/api/SimPEG.directives.rst | 1 - .../api/SimPEG.electromagnetics.base.rst | 1 - ...mPEG.electromagnetics.frequency_domain.rst | 1 - ...SimPEG.electromagnetics.natural_source.rst | 1 - docs/content/api/SimPEG.electromagnetics.rst | 27 ------------------- ...omagnetics.static.induced_polarization.rst | 1 - ...EG.electromagnetics.static.resistivity.rst | 1 - ...electromagnetics.static.self_potential.rst | 1 - ...s.static.spectral_induced_polarization.rst | 1 - ...magnetics.static.spontaneous_potential.rst | 1 - .../SimPEG.electromagnetics.static.utils.rst | 1 - .../SimPEG.electromagnetics.time_domain.rst | 1 - .../api/SimPEG.electromagnetics.utils.rst | 1 - ...gnetics.viscous_remanent_magnetization.rst | 1 - docs/content/api/SimPEG.flow.richards.rst | 1 - docs/content/api/SimPEG.meta.rst | 1 - .../api/SimPEG.potential_fields.base.rst | 1 - .../api/SimPEG.potential_fields.gravity.rst | 1 - .../api/SimPEG.potential_fields.magnetics.rst | 1 - docs/content/api/SimPEG.regularization.rst | 1 - ...SimPEG.seismic.straight_ray_tomography.rst | 1 - docs/content/api/SimPEG.utils.rst | 1 - docs/content/api/index.rst | 18 ++++++------- docs/content/api/simpeg.directives.rst | 1 + .../api/simpeg.electromagnetics.base.rst | 1 + ...mpeg.electromagnetics.frequency_domain.rst | 1 + ...simpeg.electromagnetics.natural_source.rst | 1 + docs/content/api/simpeg.electromagnetics.rst | 27 +++++++++++++++++++ ...omagnetics.static.induced_polarization.rst | 1 + ...eg.electromagnetics.static.resistivity.rst | 1 + ...electromagnetics.static.self_potential.rst | 1 + ...s.static.spectral_induced_polarization.rst | 1 + ...magnetics.static.spontaneous_potential.rst | 1 + .../simpeg.electromagnetics.static.utils.rst | 1 + .../simpeg.electromagnetics.time_domain.rst | 1 + .../api/simpeg.electromagnetics.utils.rst | 1 + ...gnetics.viscous_remanent_magnetization.rst | 1 + docs/content/api/simpeg.flow.richards.rst | 1 + .../api/{SimPEG.flow.rst => simpeg.flow.rst} | 2 +- docs/content/api/simpeg.meta.rst | 1 + .../api/simpeg.potential_fields.base.rst | 1 + .../api/simpeg.potential_fields.gravity.rst | 1 + .../api/simpeg.potential_fields.magnetics.rst | 1 + ...fields.rst => simpeg.potential_fields.rst} | 6 ++--- docs/content/api/simpeg.regularization.rst | 1 + docs/content/api/{SimPEG.rst => simpeg.rst} | 0 ...{SimPEG.seismic.rst => simpeg.seismic.rst} | 2 +- ...simpeg.seismic.straight_ray_tomography.rst | 1 + docs/content/api/simpeg.utils.rst | 1 + 49 files changed, 62 insertions(+), 62 deletions(-) delete mode 100644 docs/content/api/SimPEG.directives.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.base.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.frequency_domain.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.natural_source.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.resistivity.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.self_potential.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.utils.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.time_domain.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.utils.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst delete mode 100644 docs/content/api/SimPEG.flow.richards.rst delete mode 100644 docs/content/api/SimPEG.meta.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.base.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.gravity.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.magnetics.rst delete mode 100644 docs/content/api/SimPEG.regularization.rst delete mode 100644 docs/content/api/SimPEG.seismic.straight_ray_tomography.rst delete mode 100644 docs/content/api/SimPEG.utils.rst create mode 100644 docs/content/api/simpeg.directives.rst create mode 100644 docs/content/api/simpeg.electromagnetics.base.rst create mode 100644 docs/content/api/simpeg.electromagnetics.frequency_domain.rst create mode 100644 docs/content/api/simpeg.electromagnetics.natural_source.rst create mode 100644 docs/content/api/simpeg.electromagnetics.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.resistivity.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.self_potential.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.utils.rst create mode 100644 docs/content/api/simpeg.electromagnetics.time_domain.rst create mode 100644 docs/content/api/simpeg.electromagnetics.utils.rst create mode 100644 docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst create mode 100644 docs/content/api/simpeg.flow.richards.rst rename docs/content/api/{SimPEG.flow.rst => simpeg.flow.rst} (84%) create mode 100644 docs/content/api/simpeg.meta.rst create mode 100644 docs/content/api/simpeg.potential_fields.base.rst create mode 100644 docs/content/api/simpeg.potential_fields.gravity.rst create mode 100644 docs/content/api/simpeg.potential_fields.magnetics.rst rename docs/content/api/{SimPEG.potential_fields.rst => simpeg.potential_fields.rst} (63%) create mode 100644 docs/content/api/simpeg.regularization.rst rename docs/content/api/{SimPEG.rst => simpeg.rst} (100%) rename docs/content/api/{SimPEG.seismic.rst => simpeg.seismic.rst} (75%) create mode 100644 docs/content/api/simpeg.seismic.straight_ray_tomography.rst create mode 100644 docs/content/api/simpeg.utils.rst diff --git a/docs/content/api/SimPEG.directives.rst b/docs/content/api/SimPEG.directives.rst deleted file mode 100644 index 35999d49d0..0000000000 --- a/docs/content/api/SimPEG.directives.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.directives diff --git a/docs/content/api/SimPEG.electromagnetics.base.rst b/docs/content/api/SimPEG.electromagnetics.base.rst deleted file mode 100644 index c32e9227a1..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst b/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst deleted file mode 100644 index dc5de2199b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.natural_source.rst b/docs/content/api/SimPEG.electromagnetics.natural_source.rst deleted file mode 100644 index 5e276c525b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.natural_source.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.rst b/docs/content/api/SimPEG.electromagnetics.rst deleted file mode 100644 index b465abd441..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.rst +++ /dev/null @@ -1,27 +0,0 @@ -========================= -Electromagnetics -========================= - -Things about electromagnetics - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.induced_polarization - SimPEG.electromagnetics.static.resistivity - SimPEG.electromagnetics.static.spectral_induced_polarization - SimPEG.electromagnetics.static.self_potential - SimPEG.electromagnetics.frequency_domain - SimPEG.electromagnetics.natural_source - SimPEG.electromagnetics.time_domain - SimPEG.electromagnetics.viscous_remanent_magnetization - -Electromagnetics Utilities --------------------------- - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.utils - SimPEG.electromagnetics.utils - SimPEG.electromagnetics.base diff --git a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst deleted file mode 100644 index 94b7fdedd8..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst b/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst deleted file mode 100644 index f93b976667..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.resistivity diff --git a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst deleted file mode 100644 index ae8d5f2859..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.self_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst deleted file mode 100644 index c02a3ec010..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst deleted file mode 100644 index d5d02e8ff2..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.utils.rst b/docs/content/api/SimPEG.electromagnetics.static.utils.rst deleted file mode 100644 index 0cb346c648..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.time_domain.rst b/docs/content/api/SimPEG.electromagnetics.time_domain.rst deleted file mode 100644 index d93f52fce3..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.time_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.utils.rst b/docs/content/api/SimPEG.electromagnetics.utils.rst deleted file mode 100644 index eef7ebc5c5..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst deleted file mode 100644 index d9cf10e232..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/SimPEG.flow.richards.rst b/docs/content/api/SimPEG.flow.richards.rst deleted file mode 100644 index 9367f1d62a..0000000000 --- a/docs/content/api/SimPEG.flow.richards.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.flow.richards diff --git a/docs/content/api/SimPEG.meta.rst b/docs/content/api/SimPEG.meta.rst deleted file mode 100644 index 469456456c..0000000000 --- a/docs/content/api/SimPEG.meta.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.meta \ No newline at end of file diff --git a/docs/content/api/SimPEG.potential_fields.base.rst b/docs/content/api/SimPEG.potential_fields.base.rst deleted file mode 100644 index aba910d082..0000000000 --- a/docs/content/api/SimPEG.potential_fields.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields diff --git a/docs/content/api/SimPEG.potential_fields.gravity.rst b/docs/content/api/SimPEG.potential_fields.gravity.rst deleted file mode 100644 index 4fd6dec3f3..0000000000 --- a/docs/content/api/SimPEG.potential_fields.gravity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.gravity diff --git a/docs/content/api/SimPEG.potential_fields.magnetics.rst b/docs/content/api/SimPEG.potential_fields.magnetics.rst deleted file mode 100644 index c7dfb47af0..0000000000 --- a/docs/content/api/SimPEG.potential_fields.magnetics.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.regularization.rst b/docs/content/api/SimPEG.regularization.rst deleted file mode 100644 index 35fb57ad5a..0000000000 --- a/docs/content/api/SimPEG.regularization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.regularization diff --git a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst b/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst deleted file mode 100644 index 9590370726..0000000000 --- a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.seismic.straight_ray_tomography diff --git a/docs/content/api/SimPEG.utils.rst b/docs/content/api/SimPEG.utils.rst deleted file mode 100644 index 7791aa3277..0000000000 --- a/docs/content/api/SimPEG.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.utils diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index 8ffe60ffa9..a6f8a0d0b0 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -10,10 +10,10 @@ Geophysical Simulation Modules .. toctree:: :maxdepth: 2 - SimPEG.potential_fields - SimPEG.electromagnetics - SimPEG.flow - SimPEG.seismic + simpeg.potential_fields + simpeg.electromagnetics + simpeg.flow + simpeg.seismic SimPEG Building Blocks ====================== @@ -23,21 +23,21 @@ Base SimPEG .. toctree:: :maxdepth: 3 - SimPEG + simpeg Regularizations --------------- .. toctree:: :maxdepth: 2 - SimPEG.regularization + simpeg.regularization Directives ---------- .. toctree:: :maxdepth: 2 - SimPEG.directives + simpeg.directives Utilities --------- @@ -47,7 +47,7 @@ Classes and functions for performing useful operations. .. toctree:: :maxdepth: 2 - SimPEG.utils + simpeg.utils Meta ---- @@ -56,4 +56,4 @@ Classes for encapsulating many simulations. .. toctree:: :maxdepth: 2 - SimPEG.meta + simpeg.meta diff --git a/docs/content/api/simpeg.directives.rst b/docs/content/api/simpeg.directives.rst new file mode 100644 index 0000000000..b6c05c89d2 --- /dev/null +++ b/docs/content/api/simpeg.directives.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.directives diff --git a/docs/content/api/simpeg.electromagnetics.base.rst b/docs/content/api/simpeg.electromagnetics.base.rst new file mode 100644 index 0000000000..fb103a8f43 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.frequency_domain.rst b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst new file mode 100644 index 0000000000..c3a9a071af --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.natural_source.rst b/docs/content/api/simpeg.electromagnetics.natural_source.rst new file mode 100644 index 0000000000..cf9c7c669a --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.natural_source.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.rst b/docs/content/api/simpeg.electromagnetics.rst new file mode 100644 index 0000000000..eb04516321 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.rst @@ -0,0 +1,27 @@ +========================= +Electromagnetics +========================= + +Things about electromagnetics + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.induced_polarization + simpeg.electromagnetics.static.resistivity + simpeg.electromagnetics.static.spectral_induced_polarization + simpeg.electromagnetics.static.self_potential + simpeg.electromagnetics.frequency_domain + simpeg.electromagnetics.natural_source + simpeg.electromagnetics.time_domain + simpeg.electromagnetics.viscous_remanent_magnetization + +Electromagnetics Utilities +-------------------------- + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.utils + simpeg.electromagnetics.utils + simpeg.electromagnetics.base diff --git a/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst new file mode 100644 index 0000000000..8c29897f92 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.resistivity.rst b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst new file mode 100644 index 0000000000..1ad60928fe --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.resistivity diff --git a/docs/content/api/simpeg.electromagnetics.static.self_potential.rst b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst new file mode 100644 index 0000000000..968ab4855b --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.self_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst new file mode 100644 index 0000000000..ea0594a742 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst new file mode 100644 index 0000000000..2e7ee86039 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.utils.rst b/docs/content/api/simpeg.electromagnetics.static.utils.rst new file mode 100644 index 0000000000..7d70b243c7 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.time_domain.rst b/docs/content/api/simpeg.electromagnetics.time_domain.rst new file mode 100644 index 0000000000..4160a46799 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.time_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.utils.rst b/docs/content/api/simpeg.electromagnetics.utils.rst new file mode 100644 index 0000000000..e040bf84e2 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst new file mode 100644 index 0000000000..9eb72e4e07 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/simpeg.flow.richards.rst b/docs/content/api/simpeg.flow.richards.rst new file mode 100644 index 0000000000..f357129635 --- /dev/null +++ b/docs/content/api/simpeg.flow.richards.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.flow.richards diff --git a/docs/content/api/SimPEG.flow.rst b/docs/content/api/simpeg.flow.rst similarity index 84% rename from docs/content/api/SimPEG.flow.rst rename to docs/content/api/simpeg.flow.rst index 576e049f1b..a9c972db27 100644 --- a/docs/content/api/SimPEG.flow.rst +++ b/docs/content/api/simpeg.flow.rst @@ -7,4 +7,4 @@ Things about the fluid flow module .. toctree:: :maxdepth: 2 - SimPEG.flow.richards + simpeg.flow.richards diff --git a/docs/content/api/simpeg.meta.rst b/docs/content/api/simpeg.meta.rst new file mode 100644 index 0000000000..4fd168df84 --- /dev/null +++ b/docs/content/api/simpeg.meta.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.meta \ No newline at end of file diff --git a/docs/content/api/simpeg.potential_fields.base.rst b/docs/content/api/simpeg.potential_fields.base.rst new file mode 100644 index 0000000000..e62e05fcf3 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields diff --git a/docs/content/api/simpeg.potential_fields.gravity.rst b/docs/content/api/simpeg.potential_fields.gravity.rst new file mode 100644 index 0000000000..aa28a01ba9 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.gravity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.gravity diff --git a/docs/content/api/simpeg.potential_fields.magnetics.rst b/docs/content/api/simpeg.potential_fields.magnetics.rst new file mode 100644 index 0000000000..88bb6bfb84 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.magnetics.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.potential_fields.rst b/docs/content/api/simpeg.potential_fields.rst similarity index 63% rename from docs/content/api/SimPEG.potential_fields.rst rename to docs/content/api/simpeg.potential_fields.rst index 26d7c34c0b..f5097a9176 100644 --- a/docs/content/api/SimPEG.potential_fields.rst +++ b/docs/content/api/simpeg.potential_fields.rst @@ -5,12 +5,12 @@ Potential Fields .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.gravity - SimPEG.potential_fields.magnetics + simpeg.potential_fields.gravity + simpeg.potential_fields.magnetics Base Potential Fields --------------------- .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.base + simpeg.potential_fields.base diff --git a/docs/content/api/simpeg.regularization.rst b/docs/content/api/simpeg.regularization.rst new file mode 100644 index 0000000000..fd8099173b --- /dev/null +++ b/docs/content/api/simpeg.regularization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.regularization diff --git a/docs/content/api/SimPEG.rst b/docs/content/api/simpeg.rst similarity index 100% rename from docs/content/api/SimPEG.rst rename to docs/content/api/simpeg.rst diff --git a/docs/content/api/SimPEG.seismic.rst b/docs/content/api/simpeg.seismic.rst similarity index 75% rename from docs/content/api/SimPEG.seismic.rst rename to docs/content/api/simpeg.seismic.rst index 57f467182e..a92572ef1b 100644 --- a/docs/content/api/SimPEG.seismic.rst +++ b/docs/content/api/simpeg.seismic.rst @@ -7,4 +7,4 @@ Things about the Seismic module .. toctree:: :maxdepth: 2 - SimPEG.seismic.straight_ray_tomography + simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.seismic.straight_ray_tomography.rst b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst new file mode 100644 index 0000000000..e8bfe945d6 --- /dev/null +++ b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.utils.rst b/docs/content/api/simpeg.utils.rst new file mode 100644 index 0000000000..2ff4babe74 --- /dev/null +++ b/docs/content/api/simpeg.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.utils From c12f51efe1afb52ad9cf6ffe1e357b85c7a51c0e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 May 2024 12:28:38 -0700 Subject: [PATCH 412/455] Replace SimPEG for simpeg in getting started pages (#1447) Replace usage of `SimPEG` for `simpeg` in the Getting Started documentation pages. --- docs/content/getting_started/big_picture.rst | 22 +++++++++---------- .../contributing/code-style.rst | 2 +- .../contributing/documentation.rst | 6 ++--- .../getting_started/contributing/testing.rst | 4 ++-- .../contributing/working-with-github.rst | 2 +- docs/content/getting_started/index.rst | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/content/getting_started/big_picture.rst b/docs/content/getting_started/big_picture.rst index ee3be2f3a3..f8060f0bb6 100644 --- a/docs/content/getting_started/big_picture.rst +++ b/docs/content/getting_started/big_picture.rst @@ -99,29 +99,29 @@ empirical by nature and our software package is designed to facilitate this iterative process. To accomplish this, we have divided the inversion methodology into eight major components (See figure above). The :class:`discretize.base.BaseMesh` class handles the discretization of the -earth and also provides numerical operators. The :class:`SimPEG.survey.BaseSurvey` +earth and also provides numerical operators. The :class:`simpeg.survey.BaseSurvey` class handles the geometry of a geophysical problem as well as sources and -receivers. The :class:`SimPEG.simulation.BaseSimulation` class handles the +receivers. The :class:`simpeg.simulation.BaseSimulation` class handles the simulation of the physics for the geophysical problem of interest. The -:class:`SimPEG.simulation.BaseSimulation` creates geophysical fields given a -source from the :class:`SimPEG.survey.BaseSurvey`, interpolates these fields to +:class:`simpeg.simulation.BaseSimulation` creates geophysical fields given a +source from the :class:`simpeg.survey.BaseSurvey`, interpolates these fields to the receiver locations, and converts them to the appropriate data type, for example, by selecting only the measured components of the field. Each of these operations may have associated derivatives with respect to the model and the computed field; these are included in the calculation of the sensitivity. For -the inversion, a :class:`SimPEG.data_misfit.BaseDataMisfit` is chosen to capture +the inversion, a :class:`simpeg.data_misfit.BaseDataMisfit` is chosen to capture the goodness of fit of the predicted data and a -:class:`SimPEG.regularization.BaseRegularization` is chosen to handle the non- +:class:`simpeg.regularization.BaseRegularization` is chosen to handle the non- uniqueness. These inversion elements and an Optimization routine are combined -into an inverse problem class :class:`SimPEG.inverse_problem.BaseInvProblem`. -:class:`SimPEG.inverse_problem.BaseInvProblem` is the mathematical statement that +into an inverse problem class :class:`simpeg.inverse_problem.BaseInvProblem`. +:class:`simpeg.inverse_problem.BaseInvProblem` is the mathematical statement that will be numerically solved by running an Inversion. The -:class:`SimPEG.inversion.BaseInversion` class handles organization and +:class:`simpeg.inversion.BaseInversion` class handles organization and dispatch of directives between all of the various pieces of the framework. The arrows in the figure above indicate what each class takes as a primary -argument. For example, both the :class:`SimPEG.simulation.BaseSimulation` and -:class:`SimPEG.regularization.BaseRegularization` classes take a +argument. For example, both the :class:`simpeg.simulation.BaseSimulation` and +:class:`simpeg.regularization.BaseRegularization` classes take a :class:`discretize.base.BaseMesh` class as an argument. The diagram does not show class inheritance, as each of the base classes outlined have many subtypes that can be interchanged. The :class:`discretize.base.BaseMesh` diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/getting_started/contributing/code-style.rst index 8a021f333a..75f235d107 100644 --- a/docs/content/getting_started/contributing/code-style.rst +++ b/docs/content/getting_started/contributing/code-style.rst @@ -20,7 +20,7 @@ Run ``black`` on SimPEG directories that contain Python source files: .. code:: - black SimPEG examples tutorials tests + black simpeg examples tutorials tests Run ``flake8`` on the whole project with: diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index d4bda313e0..8ef695961a 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -39,7 +39,7 @@ For example: Second order smoothness weights for the respective dimensions. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional + mapping : simpeg.maps.IdentityMap, optional A mapping to apply to the model before regularization. reference_model : array_like, optional reference_model_in_smooth : bool, optional @@ -70,14 +70,14 @@ For example: Building the documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you would like to see the documentation changes. +If you would like to see the documentation changes. In the repo's root directory, enter the following in your terminal. .. code:: make all -Serving the documentation locally +Serving the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/getting_started/contributing/testing.rst index ba4e207e86..282bc90997 100644 --- a/docs/content/getting_started/contributing/testing.rst +++ b/docs/content/getting_started/contributing/testing.rst @@ -58,7 +58,7 @@ the ``numpy.testing`` module to check for approximate equals. For instance, import numpy as np import discretize - from SimPEG import maps + from simpeg import maps def test_map_multiplication(self): mesh = discretize.TensorMesh([2,3]) @@ -131,7 +131,7 @@ have first order convergence (e.g. the improvement in the approximation is directly related to how small :math:`\Delta x` is, while if we include the first derivative in our approximation, we expect that :math:`\|f(x) + J(x)\Delta x - f(x + \Delta x)\|` to converge at a second-order rate. For -example, all `maps have an associated derivative test `_ . An example from `test_FDEM_derivs.py `_ . An example from `test_FDEM_derivs.py `_ diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/getting_started/contributing/working-with-github.rst index 2876c1e0c2..cb944eadd0 100644 --- a/docs/content/getting_started/contributing/working-with-github.rst +++ b/docs/content/getting_started/contributing/working-with-github.rst @@ -21,7 +21,7 @@ There are two ways you can clone a repository: 1. From a terminal (checkout: https://docs.github.com/en/get-started/quickstart/set-up-git for an tutorial) :: - git clone https://github.com/YOUR-USERNAME/SimPEG + git clone https://github.com/YOUR-USERNAME/simpeg 2. Using a desktop client such as SourceTree_ or GitKraken_. diff --git a/docs/content/getting_started/index.rst b/docs/content/getting_started/index.rst index 6721be40a5..dfef8b8d96 100644 --- a/docs/content/getting_started/index.rst +++ b/docs/content/getting_started/index.rst @@ -4,7 +4,7 @@ Getting Started =============== -Here you'll find instructions on getting up and running with ``SimPEG``. +Here you'll find instructions on getting up and running with SimPEG. .. toctree:: :maxdepth: 2 From 7dec0becdc4cd3321b35472e783300d5c1eb426f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 May 2024 14:00:25 -0700 Subject: [PATCH 413/455] Check inputs for converting 3d surveys to 2d lines (#1392) Add checks to validate the `survey` and `lineID` inputs in the `convert_survey_3d_to_2d_lines` function. Add tests to check if errors are raised after passing invalid arguments. Improve the docstring of the function. --- .../static/utils/static_utils.py | 22 ++++++- tests/em/static/test_DC_Utils.py | 66 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 30edce9209..a5fcaa7e52 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -358,7 +358,7 @@ def convert_survey_3d_to_2d_lines( Defines the corresponding line ID for each datum data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} Data type for the survey. - output_indexing : bool, default=``False`` + output_indexing : bool, default=False, optional If ``True`` output a list of indexing arrays that map from the original 3D data to each 2D survey line. @@ -366,10 +366,26 @@ def convert_survey_3d_to_2d_lines( ------- survey_list : list of simpeg.electromagnetics.static.resistivity.Survey A list of 2D survey objects - out_indices_list : list of numpy.ndarray + out_indices_list : list of numpy.ndarray, optional A list of indexing arrays that map from the original 3D data to each 2D - survey line. + survey line. Will be returned only if ``output_indexing`` is set to + True. """ + # Check if the survey is 3D + if (ndims := survey.locations_a.shape[1]) != 3: + raise ValueError(f"Invalid {ndims}D 'survey'. It should be a 3D survey.") + # Checks on the passed lineID array + if (ndims := lineID.ndim) != 1: + raise ValueError( + f"Invalid 'lineID' array with '{ndims}' dimensions. " + "It should be a 1D array." + ) + if (size := lineID.size) != survey.nD: + raise ValueError( + f"Invalid 'lineID' array with '{size}' elements. " + "It should have the same number of elements as data " + f"in the survey ('{survey.nD}')." + ) # Find all unique line id unique_lineID = np.unique(lineID) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index bc872fcc85..5e98513a81 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -1,5 +1,6 @@ # import matplotlib # matplotlib.use('Agg') +import pytest import unittest import numpy as np import discretize @@ -345,5 +346,70 @@ def test_convert_to_2d(self): self.assertEqual(survey.locations_a[0, 0], 0) +class TestConvertTo2DInvalidInputs: + """ + Test convert_survey_3d_to_2d_lines after passing invalid inputs. + """ + + @pytest.fixture + def survey_3d(self): + """Sample 3D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0, 0]]), + locations_n=np.array([[100, 0, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0, 0]), + location_b=np.array([50, 0, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + @pytest.fixture + def survey_2d(self): + """Sample 2D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0]), + location_b=np.array([50, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + def test_invalid_survey(self, survey_2d): + """ + Test if error is raised when passing an invalid survey (2D survey) + """ + line_ids = np.ones(survey_2d.nD) + with pytest.raises(ValueError, match="Invalid 2D 'survey'"): + utils.convert_survey_3d_to_2d_lines(survey_2d, line_ids) + + def test_invalid_line_ids_wrong_dims(self, survey_3d): + """ + Test if error is raised after invalid line_ids with wrong dimensions. + """ + line_ids = np.atleast_2d(np.ones(survey_3d.nD)) + msg = "Invalid 'lineID' array with '2' dimensions. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + def test_invalid_line_ids_wrong_size(self, survey_3d): + """ + Test if error is raised after an invalid line_ids with wrong size. + """ + size = survey_3d.nD - 1 + line_ids = np.ones(size) + msg = f"Invalid 'lineID' array with '{size}' elements. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + if __name__ == "__main__": unittest.main() From 6f35ed29dc8c61785f759de3842646fcbcbebfc7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 15 May 2024 09:25:36 -0700 Subject: [PATCH 414/455] Always use Pydata Sphinx theme for building docs (#1445) Always use the Pydata Sphinx theme for building the docs. Remove the `try` statement that used to check if the theme was installed and defaulted to the default theme if it wasn't. --- docs/conf.py | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dde265d6d5..30d36e1446 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -237,62 +237,58 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] -try: - import pydata_sphinx_theme - - html_theme = "pydata_sphinx_theme" - - # If false, no module index is generated. - html_use_modindex = True - - html_theme_options = { - "external_links": external_links, - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/simpeg/simpeg", - "icon": "fab fa-github", - }, - { - "name": "Mattermost", - "url": "https://mattermost.softwareunderground.org/simpeg", - "icon": "fas fa-comment", - }, - { - "name": "Discourse", - "url": "https://simpeg.discourse.group/", - "icon": "fab fa-discourse", - }, - { - "name": "Youtube", - "url": "https://www.youtube.com/c/geoscixyz", - "icon": "fab fa-youtube", - }, - ], - "use_edit_page_button": False, - "collapse_navigation": True, - "analytics": { - "plausible_analytics_domain": "docs.simpeg.xyz", - "plausible_analytics_url": "https://plausible.io/js/script.js", +# Use Pydata Sphinx theme +html_theme = "pydata_sphinx_theme" + +# If false, no module index is generated. +html_use_modindex = True + +html_theme_options = { + "external_links": external_links, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/simpeg/simpeg", + "icon": "fab fa-github", + }, + { + "name": "Mattermost", + "url": "https://mattermost.softwareunderground.org/simpeg", + "icon": "fas fa-comment", }, - "navbar_align": "left", # make elements closer to logo on the left - } - html_logo = "images/simpeg-logo.png" - - html_static_path = ["_static"] - - html_css_files = [ - "css/custom.css", - ] - - html_context = { - "github_user": "simpeg", - "github_repo": "simpeg", - "github_version": "main", - "doc_path": "docs", - } -except Exception: - html_theme = "default" + { + "name": "Discourse", + "url": "https://simpeg.discourse.group/", + "icon": "fab fa-discourse", + }, + { + "name": "Youtube", + "url": "https://www.youtube.com/c/geoscixyz", + "icon": "fab fa-youtube", + }, + ], + "use_edit_page_button": False, + "collapse_navigation": True, + "analytics": { + "plausible_analytics_domain": "docs.simpeg.xyz", + "plausible_analytics_url": "https://plausible.io/js/script.js", + }, + "navbar_align": "left", # make elements closer to logo on the left +} +html_logo = "images/simpeg-logo.png" + +html_static_path = ["_static"] + +html_css_files = [ + "css/custom.css", +] + +html_context = { + "github_user": "simpeg", + "github_repo": "simpeg", + "github_version": "main", + "doc_path": "docs", +} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From e13a25f096587a5bd17a81893a3d7164b4c909e4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 15 May 2024 11:30:38 -0700 Subject: [PATCH 415/455] Simplify interface of UniformBackgroundField (#1421) Remove the `**kwargs` from the constructor of `UniformBackgroundField`. Make `receiver_list`, `amplitude`, `inclination` and `declination` as required arguments without any default value. Include type hints in the signature of the constructor. Add check for type of receivers to the constructor of `UniformBackgroundField`. Add test that checks the value of `UniformBackgroundField.b0`, passing `receiver_list` as an actual list of point receivers and as `None`. Add test that checks if the error is raised after passing invalid receiver type. Minor improvements to the docstring of the class. --- simpeg/potential_fields/magnetics/sources.py | 60 +++++++++++-------- tests/pf/test_forward_Mag_Linear.py | 4 +- tests/pf/test_mag_uniform_background_field.py | 52 ++++++++++++++-- tests/pf/test_survey_counting.py | 4 +- tests/utils/test_io_utils.py | 4 +- 5 files changed, 90 insertions(+), 34 deletions(-) diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index 0e4000e9c2..df9027f38a 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,9 @@ +from __future__ import annotations from ...survey import BaseSrc -from simpeg.utils.mat_utils import dip_azimuth2cartesian -from simpeg.utils.code_utils import deprecate_class, validate_float +from ...utils.mat_utils import dip_azimuth2cartesian +from ...utils.code_utils import deprecate_class, validate_float, validate_list_of_types + +from .receivers import Point class UniformBackgroundField(BaseSrc): @@ -11,39 +14,46 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- - receiver_list : list of simpeg.potential_fields.magnetics.Point - amplitude : float, optional - amplitude of the inducing backgound field, usually this is in units of nT. - inclination : float, optional - Dip angle in degrees from the horizon, positive points into the earth. - declination : float, optional + receiver_list : simpeg.potential_fields.magnetics.Point, list of simpeg.potential_fields.magnetics.Point or None + Point magnetic receivers. + amplitude : float + Amplitude of the inducing background field, usually this is in + units of nT. + inclination : float + Dip angle in degrees from the horizon, positive value into the earth. + declination : float Azimuthal angle in degrees from north, positive clockwise. """ def __init__( self, - receiver_list=None, - amplitude=50000.0, - inclination=90.0, - declination=0.0, - **kwargs, + receiver_list: Point | list[Point] | None, + amplitude: float, + inclination: float, + declination: float, ): - # Raise errors on 'parameters' argument - # The parameters argument was supported in the deprecated SourceField - # class. We would like to raise an error in case the user passes it - # so the class doesn't behave differently than expected. - if (key := "parameters") in kwargs: - raise TypeError( - f"'{key}' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) - self.amplitude = amplitude self.inclination = inclination self.declination = declination + super().__init__(receiver_list=receiver_list) + + @property + def receiver_list(self): + """ + List of receivers associated with the survey. - super().__init__(receiver_list=receiver_list, **kwargs) + Returns + ------- + list of SimPEG.potential_fields.magnetics.Point + List of magnetic receivers associated with the survey + """ + return self._receiver_list + + @receiver_list.setter + def receiver_list(self, value): + self._receiver_list = validate_list_of_types( + "receiver_list", value, Point, ensure_unique=True + ) @property def amplitude(self): diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 21f0570189..45ff17ba8c 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -504,7 +504,9 @@ def test_removed_modeltype(): mesh = discretize.TensorMesh(h) receiver_location = np.array([[0, 0, 100]]) receiver = mag.Point(receiver_location, components="tmi") - background_field = mag.UniformBackgroundField(receiver_list=[receiver]) + background_field = mag.UniformBackgroundField( + receiver_list=[receiver], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(background_field) mapping = maps.IdentityMap(mesh, nP=mesh.n_cells) sim = mag.Simulation3DIntegral(mesh, survey=survey, chiMap=mapping) diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index 785be3355e..feeb65e909 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -3,7 +3,8 @@ """ import pytest -from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField +import numpy as np +from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField, Point def test_invalid_parameters_argument(): @@ -11,11 +12,7 @@ def test_invalid_parameters_argument(): Test if error is raised after passing 'parameters' as argument """ parameters = (1, 35, 60) - msg = ( - "'parameters' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) + msg = r"__init__\(\) got an unexpected keyword argument 'parameters'" with pytest.raises(TypeError, match=msg): UniformBackgroundField(parameters=parameters) @@ -27,3 +24,46 @@ def test_deprecated_source_field(): msg = "SourceField has been removed, please use UniformBackgroundField." with pytest.raises(NotImplementedError, match=msg): SourceField() + + +@pytest.mark.parametrize("receiver_as_list", (True, False)) +def test_invalid_receiver_type(receiver_as_list): + """ + Test if error is raised after passing invalid type of receivers + """ + receiver_invalid = np.array([[1.0, 1.0, 1.0]]) + if receiver_as_list: + receiver_valid = Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi") + receiver_list = [receiver_valid, receiver_invalid] + else: + receiver_list = receiver_invalid + msg = f"'receiver_list' must be a list of {Point}" + with pytest.raises(TypeError, match=msg): + UniformBackgroundField( + receiver_list=receiver_list, + amplitude=55_000, + inclination=45, + declination=30, + ) + + +@pytest.mark.parametrize( + "receiver_list", + (None, [Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi")]), + ids=("None", "Point"), +) +def test_value_b0(receiver_list): + """ + Test UniformBackgroundField.b0 value + """ + amplitude = 55_000 + inclination = 45 + declination = 10 + expected_b0 = (6753.3292182935065, 38300.03321760104, -38890.87296526011) + uniform_background_field = UniformBackgroundField( + receiver_list=receiver_list, + amplitude=amplitude, + inclination=inclination, + declination=declination, + ) + np.testing.assert_allclose(uniform_background_field.b0, expected_b0) diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py index 84aa334561..d0e0d71002 100644 --- a/tests/pf/test_survey_counting.py +++ b/tests/pf/test_survey_counting.py @@ -27,7 +27,9 @@ def test_magnetics_survey(): rx1 = mag.Point(rx_locs, components=rx_components) rx2 = mag.Point(rx_locs, components="tmi") - src = mag.UniformBackgroundField([rx1, rx2]) + src = mag.UniformBackgroundField( + receiver_list=[rx1, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(src) assert rx1.nD == 60 diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 9e9a07f206..105ade7451 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -256,7 +256,9 @@ def setUp(self): self.std = std rx2 = magnetics.receivers.Point(xyz, components="tmi") - src_bad = magnetics.sources.UniformBackgroundField([rx, rx2]) + src_bad = magnetics.sources.UniformBackgroundField( + receiver_list=[rx, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey_bad = magnetics.survey.Survey(src_bad) self.survey_bad = survey_bad From 80e3b1807d8dedac04cb8aa1731957462ffa187c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 15 May 2024 16:35:17 -0600 Subject: [PATCH 416/455] Ensure the queue's are joined when the meta simulation is joined. (#1464) Join the multiprocessing queues in the `MultiprocessingMetaSimulation` when the simulation is joined. --- simpeg/meta/multiprocessing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index 23e025688b..f5aceceda6 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -26,7 +26,12 @@ def __init__(self, item_id, t_queue, r_queue): def __del__(self): # Tell the child process that this object is no longer needed in its cache. - self.t_queue.put(("del_item", (self.item_id,))) + try: + self.t_queue.put(("del_item", (self.item_id,))) + except ValueError: + # if the queue was already closed it will throw a value error + # so catch it here gracefully and continue on. + pass class _SimulationProcess(Process): @@ -169,6 +174,15 @@ def result(self): self._check_closed() return self.result_queue.get() + def join(self, timeout=None): + self._check_closed() + self.task_queue.put(None) + self.task_queue.close() + self.result_queue.close() + self.task_queue.join_thread() + self.result_queue.join_thread() + super().join(timeout=timeout) + class MultiprocessingMetaSimulation(MetaSimulation): """Multiprocessing version of simulation of simulations. @@ -193,11 +207,11 @@ class MultiprocessingMetaSimulation(MetaSimulation): ... sim = MultiprocessingMetaSimulation(...) ... sim.dpred(model) - You must also be sure to call sim.close() before discarding + You must also be sure to call `sim.join()` before discarding this worker to kill the subprocesses that are created, as you would with - any other multiprocessing queue. + any other multiprocessing process. - >>> sim.close() + >>> sim.join() Parameters ---------- @@ -344,7 +358,6 @@ def getJtJdiag(self, m, W=None, f=None): def join(self, timeout=None): for p in self._sim_processes: if p.is_alive(): - p.task_queue.put(None) p.join(timeout=timeout) From 50306eebab58dc1255548c791eab91df76b652cb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 16 May 2024 11:34:59 -0700 Subject: [PATCH 417/455] Add maintenance issue template (#1468) Add a new issue template for opening maintenance issues. --- .github/ISSUE_TEMPLATE/maintenance.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/maintenance.md diff --git a/.github/ISSUE_TEMPLATE/maintenance.md b/.github/ISSUE_TEMPLATE/maintenance.md new file mode 100644 index 0000000000..ffbb7cd4a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.md @@ -0,0 +1,18 @@ +--- +name: Maintenance +about: "Maintainers only: Issues for maintenance tasks" +title: "MNT: " +labels: "maintenance" +assignees: "" +--- + +**Description of the maintenance task** + + From dcec9d5fbbfbb8691769e41da16a790368959958 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 May 2024 09:21:50 -0700 Subject: [PATCH 418/455] Add instructions to update the environment (#1462) Add instructions to the Contributing to SimPEG pages on how to update the environment once it's already created. --- .../contributing/setting-up-environment.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index 0ec601f540..5775920d31 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -138,3 +138,19 @@ you want to commit them nonetheless. .. _pre-commit: https://pre-commit.com/ .. _Black: https://black.readthedocs.io .. _flake8: https://flake8.pycqa.org + + +Update your environment +----------------------- + +Every once in a while, the minimum versions of the packages in the +``environment.yml`` file get updated. After this happens, it's better to update +the ``simpeg-test`` environment we have created. This way we ensure that we are +checking the style and testing our code using those updated versions. + +To update our environment we need to navigate to the directory where you +:ref:`cloned SimPEG's repository ` and run: + +.. code:: + + conda env update -f environment_test.yml From 67c041699c19185a52c01960fed9fd29cc59ce57 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 May 2024 09:22:56 -0700 Subject: [PATCH 419/455] Stop recommending mamba for installing simpeg (#1463) Since latest versions of `conda` make use of `libmamba` that achieves the same performance as `mamba` while installing packages and creating environments, we can stop recommending `mamba` to our users as a better alternative for `conda`. Update the admonitions in the instructions to install simpeg and to set up the environment for contributors. Mention the minimum version of `conda` that uses `libmamba` by default. Replace recommendations for Mambaforge to Miniforge. --- .../contributing/setting-up-environment.rst | 24 ++++++++++------ docs/content/getting_started/installing.rst | 28 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index 5775920d31..d775983dff 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -8,13 +8,13 @@ Install Python First you will need to install Python. You can find instructions in :ref:`installing_python`. We highly encourage to install Anaconda_ or -Mambaforge_. +Miniforge_. Create environment ------------------ To get started developing SimPEG we recommend setting up an environment using -the ``conda`` (or ``mamba``) package manager that mimics the testing +the ``conda`` package manager that mimics the testing environment used for continuous integration testing. Most of the packages that we use are available through the ``conda-forge`` project. This will ensure you have all of the necessary packages to both develop SimPEG and run tests @@ -30,11 +30,19 @@ repository ` and run: .. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. + Once the environment is successfully created, you can *activate* it with @@ -72,7 +80,7 @@ This practice also allows you to uninstall SimPEG if so desired: a way to install SimPEG for developers. .. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge Check your installation ----------------------- diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst index 14adff1570..06724787e7 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -10,7 +10,7 @@ Prerequisite: Installing Python =============================== SimPEG is written in Python_! -We highly recommend installing it using Anaconda_ (or the alternative Mambaforge_). +We highly recommend installing it using Anaconda_ (or the alternative Miniforge_). It installs `Python `_, `Jupyter `_ and other core Python libraries for scientific computing. @@ -30,7 +30,7 @@ recommend checking out `Software Carpentry `_. .. _Python: https://www.python.org/ .. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge .. _installing_simpeg: @@ -42,21 +42,29 @@ Conda Forge ----------- SimPEG is available through `conda-forge` and you can install is using the -`conda package manager `_ that comes with the Anaconda -distribution: +`conda package manager `_ that comes with the Anaconda_ +or Miniforge_ distributions: .. code:: conda install SimPEG --channel conda-forge -Installing through `conda`/`mamba` is our recommended method of installation. +Installing through `conda` is our recommended method of installation. .. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). + + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. PyPi ---- From 712cfda4457651142dec6011b38547a1300534df Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 13:10:32 -0700 Subject: [PATCH 420/455] Fix bug on arguments of beta estimator directives (#1460) Fix bug in the constructor of beta estimator directive classes: the `seed` argument was being passed as a positional argument to the parent's constructor, which would assign it to the `n_pw_iter` attribute instead to the `seed`, leading to unwanted behaviours. Pass the arguments to the constructor through keywords to make it more explicit and less prone to errors. Add tests that would fail without this bugfix. Remove the `n_pw_iter` and the `method` arguments from the constructor of `BaseBetaEstimator`. --- simpeg/directives/directives.py | 6 ++---- tests/base/test_directives.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 156f1acb33..1fd28d2467 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -355,9 +355,7 @@ class BaseBetaEstimator(InversionDirective): def __init__( self, beta0_ratio=1.0, - n_pw_iter=4, seed=None, - method="power_iteration", **kwargs, ): super().__init__(**kwargs) @@ -452,7 +450,7 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) def initialize(self): if self.seed is not None: @@ -522,7 +520,7 @@ class BetaEstimate_ByEig(BaseBetaEstimator): """ def __init__(self, beta0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) self.n_pw_iter = n_pw_iter @property diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 474fba332a..aa83489187 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -397,5 +397,34 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method +class TestBetaEstimatorArguments: + """ + Test if arguments are assigned in beta estimator directives. + These tests catch the bug described and fixed in #1460. + """ + + def test_beta_estimate_by_eig(self): + """Test on directives.BetaEstimate_ByEig.""" + beta0_ratio = 3.0 + n_pw_iter = 3 + seed = 42 + directive = directives.BetaEstimate_ByEig( + beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.n_pw_iter == n_pw_iter + assert directive.seed == seed + + def test_beta_estimate_max_derivative(self): + """Test on directives.BetaEstimateMaxDerivative.""" + beta0_ratio = 3.0 + seed = 42 + directive = directives.BetaEstimateMaxDerivative( + beta0_ratio=beta0_ratio, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.seed == seed + + if __name__ == "__main__": unittest.main() From bc9a424c531dc76f05dc4e799ce4c7981dda0044 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 14:41:09 -0700 Subject: [PATCH 421/455] Improve interface for DC Dipole source (#1393) Remove `**kwargs` from the constructor of the class, add `current` as one of the arguments. Improve the logic and error messages for the `location`, `location_a` and `location_b` arguments. Add tests for these new checks. Update docstring of the constructor to reflect latest changes. Fix the expected shape of the `location_a` and `location_b` arguments. --- .../static/resistivity/sources.py | 80 +++++---- tests/em/static/test_dc_sources_interface.py | 152 ++++++++++++++++++ 2 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 tests/em/static/test_dc_sources_interface.py diff --git a/simpeg/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py index dd55089689..ee46681d3b 100644 --- a/simpeg/electromagnetics/static/resistivity/sources.py +++ b/simpeg/electromagnetics/static/resistivity/sources.py @@ -139,13 +139,18 @@ class Dipole(BaseSrc): ---------- receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers - location_a : (n_source, dim) numpy.array_like - A electrode locations; remember to set 'location_b' keyword argument to define N electrode locations. - location_b : (n_source, dim) numpy.array_like - B electrode locations; remember to set 'location_a' keyword argument to define M electrode locations. - location : list or tuple of length 2 of numpy.array_like - A and B electrode locations. In this case, do not set the 'location_a' and 'location_b' - keyword arguments. And we supply a list or tuple of the form [location_a, location_b]. + location_a : (dim) array_like + A electrode locations; remember to set ``location_b`` keyword argument + to define B electrode location. + location_b : (dim) array_like + B electrode locations; remember to set ``location_a`` keyword argument + to define A electrode location. + location : tuple of array_like, optional + A and B electrode locations. If ``location_a`` and ``location_b`` are + provided, don't pass values to this argument. Otherwise, provide + a tuple of the form ``(location_a, location_b)``. + current : float, optional + Current amplitude in :math:`A` that goes through each electrode. """ def __init__( @@ -154,41 +159,46 @@ def __init__( location_a=None, location_b=None, location=None, - **kwargs, + current=1.0, ): - if "current" in kwargs.keys(): - value = kwargs.pop("current") - current = [value, -value] - else: - current = [1.0, -1.0] - - # if location_a set, then use location_a, location_b - if location_a is not None: - if location_b is None: - raise ValueError( - "For a dipole source both location_a and location_b " "must be set" + if location is None and location_a is None and location_b is None: + raise TypeError( + "Found 'location', 'location_a' and 'location_b' as None. " + "Please specify 'location', or 'location_a' and 'location_b' " + "when defining a dipole source." + ) + if location is not None and (location_a is not None or location_b is not None): + raise TypeError( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + if location is None: + if location_a is None: + raise TypeError( + "Invalid 'location_a' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - - if location is not None: - raise ValueError( - "Cannot set both location and location_a, location_b. " - "Please provide either location=(location_a, location_b) " - "or both location_a=location_a, location_b=location_b" + if location_b is None: + raise TypeError( + "Invalid 'location_b' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - location = [location_a, location_b] - elif location is not None: - if len(location) != 2: - raise ValueError( - "location must be a list or tuple of length 2: " - "[location_a, location_b]. The input location has " - f"length {len(location)}" - ) + if len(location) != 2: + raise ValueError( + "location must be a list or tuple of length 2: " + "[location_a, location_b]. The input location has " + f"length {len(location)}" + ) - # instantiate super().__init__( - receiver_list=receiver_list, location=location, current=current, **kwargs + receiver_list=receiver_list, + location=location, + current=[current, -current], ) def __repr__(self): diff --git a/tests/em/static/test_dc_sources_interface.py b/tests/em/static/test_dc_sources_interface.py new file mode 100644 index 0000000000..74a85ea5e1 --- /dev/null +++ b/tests/em/static/test_dc_sources_interface.py @@ -0,0 +1,152 @@ +""" +Test interface for some DC sources. +""" + +import pytest +import numpy as np +from simpeg.electromagnetics.static import resistivity as dc + + +class TestDipoleLocations: + r""" + Test the location, location_a and location_b arguments for the Dipole + + Considering that `location`, `location_a`, `location_b` can be None or not + None, then we have 8 different possible combinations. + + + .. code:: + + | location | location_a | location_b | Result | + |----------|------------|------------|--------| + | None | None | None | Error | + | None | None | not None | Error | + | None | not None | None | Error | + | None | not None | not None | Run | + | not None | None | None | Run | + | not None | None | not None | Error | + | not None | not None | None | Error | + | not None | not None | not None | Error | + """ + + @pytest.fixture + def receiver(self): + """Sample DC dipole receiver.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + return receiver + + def test_all_nones(self, receiver): + """ + Test error being raised when passing all location as None + """ + msg = "Found 'location', 'location_a' and 'location_b' as None. " + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location_a=None, + location_b=None, + location=None, + ) + + @pytest.mark.parametrize("electrode", ("a", "b", "both")) + def test_not_nones(self, receiver, electrode): + """ + Test error after location as not None, and location_a and/or location_b + as not None + """ + msg = ( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if electrode == "a": + kwargs = dict(location_a=electrode_a, location_b=None) + elif electrode == "b": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=electrode_b) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + **kwargs, + ) + + @pytest.mark.parametrize("none_electrode", ("a", "b")) + def test_single_location_as_none(self, receiver, none_electrode): + """ + Test error after location is None and one of location_a or location_b + is also None. + """ + msg = ( + f"Invalid 'location_{none_electrode}' set to None. " + "When 'location' is None, both 'location_a' and 'location_b' " + "should be set to a value different than None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if none_electrode == "a": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=None) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=None, + **kwargs, + ) + + def test_location_none(self, receiver): + """ + Test if object is correctly initialized with location set to None + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=electrode_a, + location_b=electrode_b, + location=None, + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + def test_location_not_none(self, receiver): + """ + Test if object is correctly initialized with location is set + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + @pytest.mark.parametrize("length", (0, 1, 3)) + def test_location_invalid_num_elements(self, length, receiver): + """ + Test error after passing location with invalid number of elements + """ + if length == 0: + location = () + elif length == 1: + location = (np.array([1.0, 0.0]),) + else: + location = ( + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + ) + msg = "location must be a list or tuple of length 2" + with pytest.raises(ValueError, match=msg): + dc.sources.Dipole(receiver_list=[receiver], location=location) From 079d870afda7cc50175e124dd7ef1279268f1fdc Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 16:45:19 -0700 Subject: [PATCH 422/455] Use Numpy random number generator in codebase (#1394) Replace the usage of legacy `numpy.random.seed()` in SimPEG codebase for the Numpy random number generator object that can be created through the `numpy.random.default_rng()` function. Add a new `typing` module that contain new type aliases, and create a new `RandomSeed` type alias for variables accepted by `numpy.random.default_rng()`. Extend the accepted values for `seed` arguments in several methods and functions to be `None` or `RandomSeed`. Apply these replacements to directives, `eigenvalue_by_power_iteration()`, `create_random_model()`, utility functions on `test_utils.py` and usage of `eigenvalue_by_power_iteration()` in tests. Update documentation and type hint of `make_synthetic_model()` allowing it to get a Numpy `Generator` as `random_seed`. Add a new documentation page for the new `typing` module. --- docs/content/api/index.rst | 11 ++ docs/content/api/simpeg.typing.rst | 1 + simpeg/__init__.py | 1 + simpeg/directives/directives.py | 104 +++++++++++++----- simpeg/directives/sim_directives.py | 5 +- .../natural_source/utils/test_utils.py | 8 +- simpeg/simulation.py | 12 +- simpeg/typing/__init__.py | 61 ++++++++++ simpeg/utils/mat_utils.py | 20 ++-- simpeg/utils/model_builder.py | 29 +++-- tests/base/test_directives.py | 32 ++++++ tests/utils/test_mat_utils.py | 8 +- 12 files changed, 231 insertions(+), 61 deletions(-) create mode 100644 docs/content/api/simpeg.typing.rst create mode 100644 simpeg/typing/__init__.py diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index a6f8a0d0b0..e401b4422d 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -57,3 +57,14 @@ Classes for encapsulating many simulations. :maxdepth: 2 simpeg.meta + + +Typing +------ + +PEP 484 type aliases used in ``simpeg``. + +.. toctree:: + :maxdepth: 1 + + simpeg.typing \ No newline at end of file diff --git a/docs/content/api/simpeg.typing.rst b/docs/content/api/simpeg.typing.rst new file mode 100644 index 0000000000..44c57f983f --- /dev/null +++ b/docs/content/api/simpeg.typing.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.typing diff --git a/simpeg/__init__.py b/simpeg/__init__.py index f46c2b0e0b..0dc76dab2d 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -149,6 +149,7 @@ from . import regularization from . import survey from . import simulation +from . import typing from . import utils from .utils import mkvc diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 1fd28d2467..86d8728f4b 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,8 +1,10 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import matplotlib.pyplot as plt import warnings import os import scipy.sparse as sp +from ..typing import RandomSeed from ..data_misfit import BaseDataMisfit from ..objective_function import ComboObjectiveFunction from ..maps import IdentityMap, Wires @@ -347,15 +349,17 @@ class BaseBetaEstimator(InversionDirective): ---------- beta0_ratio : float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. """ def __init__( self, beta0_ratio=1.0, - seed=None, + seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) @@ -384,14 +388,20 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def validate(self, directive_list): @@ -418,8 +428,10 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): ---------- beta0_ratio: float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. Notes ----- @@ -449,19 +461,18 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): + def __init__(self, beta0_ratio=1.0, seed: RandomSeed | None = None, **kwargs): super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model - x0 = np.random.rand(*m.shape) + x0 = rng.random(size=m.shape) phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() dm = x0 / x0.max() * m.max() phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() @@ -489,8 +500,10 @@ class BetaEstimate_ByEig(BaseBetaEstimator): Desired ratio between data misfit and model objective function at initial beta iteration. n_pw_iter : int Number of power iterations used to estimate largest eigenvalues. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. Notes ----- @@ -519,7 +532,13 @@ class BetaEstimate_ByEig(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + beta0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) self.n_pw_iter = n_pw_iter @@ -539,8 +558,7 @@ def n_pw_iter(self, value): self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -551,11 +569,13 @@ def initialize(self): self.dmisfit, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) reg_eigenvalue = eigenvalue_by_power_iteration( self.reg, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) @@ -639,7 +659,13 @@ class AlphasSmoothEstimate_ByEig(InversionDirective): The highest eigenvalue are estimated through power iterations and Rayleigh quotient. """ - def __init__(self, alpha0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + alpha0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.alpha0_ratio = alpha0_ratio self.n_pw_iter = n_pw_iter @@ -681,20 +707,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) smoothness = [] smallness = [] @@ -729,6 +760,7 @@ def initialize(self): smallness[0], self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) @@ -744,6 +776,7 @@ def initialize(self): obj, self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) ratio = smallness_eigenvalue / smooth_i_eigenvalue @@ -765,7 +798,13 @@ class ScalingMultipleDataMisfits_ByEig(InversionDirective): The highest eigenvalue are estimated through power iterations and Rayleigh quotient. """ - def __init__(self, chi0_ratio=None, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + chi0_ratio=None, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.chi0_ratio = chi0_ratio self.n_pw_iter = n_pw_iter @@ -807,20 +846,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the scaling parameter.") @@ -843,7 +887,7 @@ def initialize(self): dm_eigenvalue_list = [] for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m)] + dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m, seed=rng)] self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] self.chi0 = self.chi0 / np.sum(self.chi0) diff --git a/simpeg/directives/sim_directives.py b/simpeg/directives/sim_directives.py index 0a3464717d..480cda76ee 100644 --- a/simpeg/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -245,8 +245,7 @@ def initialize(self): :rtype: float :return: beta0 """ - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -271,6 +270,7 @@ def initialize(self): dmis, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) @@ -279,6 +279,7 @@ def initialize(self): reg, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 9cde461e5a..878ddaea82 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -12,7 +12,6 @@ from ..simulation import Simulation3DPrimarySecondary from .data_utils import appResPhs -np.random.seed(1100) # Define the tolerances TOLr = 5e-2 TOLp = 5e-2 @@ -510,14 +509,15 @@ def getInputs(): return M, freqs, rx_loc, elev -def random(conds): +def random(conds, seed=42): """Returns a random model based on the inputs""" + rng = np.random.default_rng(seed=seed) M, freqs, rx_loc, elev = getInputs() - # Backround + # Background sigBG = np.ones(M.nC) * conds # Add randomness to the model (10% of the value). - sig = np.exp(np.log(sigBG) + np.random.randn(M.nC) * (conds) * 1e-1) + sig = np.exp(np.log(sigBG) + rng.random(size=M.nC) * (conds) * 1e-1) return (M, freqs, sig, sigBG, rx_loc) diff --git a/simpeg/simulation.py b/simpeg/simulation.py index cc751647cd..e1091b1997 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -2,6 +2,7 @@ Define simulation classes. """ +from __future__ import annotations # needed to use type operands in Python 3.8 import os import inspect import numpy as np @@ -12,6 +13,7 @@ from discretize.utils import unpack_widths, sdiag from . import props +from .typing import RandomSeed from .data import SyntheticData, Data from .survey import BaseSurvey from .utils import ( @@ -465,7 +467,7 @@ def make_synthetic_data( noise_floor=0.0, f=None, add_noise=False, - random_seed=None, + random_seed: RandomSeed | None = None, **kwargs, ): r"""Make synthetic data for the model and Gaussian noise provided. @@ -490,8 +492,10 @@ def make_synthetic_data( forward problem to obtain noiseless data. add_noise : bool Whether to add gaussian noise to the synthetic data or not. - random_seed : int, optional - Random seed to pass to :py:class:`numpy.random.default_rng`. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int or + a predefined Numpy random number generator (see + ``numpy.random.default_rng``). Returns ------- @@ -511,8 +515,8 @@ def make_synthetic_data( dclean = self.dpred(m, f=f) if add_noise is True: - std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) random_num_generator = np.random.default_rng(seed=random_seed) + std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) noise = random_num_generator.normal(loc=0, scale=std, size=dclean.shape) dobs = dclean + noise else: diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py new file mode 100644 index 0000000000..07975782bd --- /dev/null +++ b/simpeg/typing/__init__.py @@ -0,0 +1,61 @@ +""" +============================= +Typing (:mod:`simpeg.typing`) +============================= + +This module provides additional `PEP 484 `_ +type aliases used in ``simpeg``'s codebase. + +API +--- + +.. autosummary:: + :toctree: generated/ + + RandomSeed + +""" + +from __future__ import annotations +import numpy as np +import numpy.typing as npt +from typing import Union + +# Use try and except to support Python<3.10 +try: + from typing import TypeAlias + + RandomSeed: TypeAlias = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] +except ImportError: + RandomSeed = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] + +RandomSeed.__doc__ = """ +A ``typing.Union`` for random seeds and Numpy's random number generators. + +These type of variables can be used throughout ``simpeg`` to control random +states of functions and classes. These variables can either be an integer that +will be used as a ``seed`` to define a Numpy's :class:`numpy.random.Generator`, or +a predefined random number generator. + +Examples +-------- + +>>> import numpy as np +>>> from simpeg.typing import RandomSeed +>>> +>>> def my_function(seed: RandomSeed = None): +... rng = np.random.default_rng(seed=seed) +... ... +""" diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 3614a15c2f..e57b4a34f0 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,5 +1,7 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np from .code_utils import deprecate_function +from ..typing import RandomSeed from discretize.utils import ( # noqa: F401 Zero, Identity, @@ -129,7 +131,11 @@ def unique_rows(M): def eigenvalue_by_power_iteration( - combo_objfct, model, n_pw_iter=4, fields_list=None, seed=None + combo_objfct, + model, + n_pw_iter=4, + fields_list=None, + seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. @@ -150,8 +156,10 @@ def eigenvalue_by_power_iteration( they will be evaluated within the function. If combo_objfct mixs data misfit and regularization terms, the list should contains simpeg.fields for the data misfit terms and None for the regularization term. - seed : int - Random seed for the initial random guess of eigenvector. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for the initial random guess of eigenvector. It can either + be an int, a predefined Numpy random number generator, or any valid + input to ``numpy.random.default_rng``. Returns ------- @@ -176,12 +184,10 @@ def eigenvalue_by_power_iteration( selected from a uniform distribution. """ - - if seed is not None: - np.random.seed(seed) + rng = np.random.default_rng(seed=seed) # Initial guess for eigen-vector - x0 = np.random.rand(*model.shape) + x0 = rng.random(size=model.shape) x0 = x0 / np.linalg.norm(x0) # transform to ComboObjectiveFunction if required diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 285fa976c1..c3a68abcec 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,3 +1,4 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -5,6 +6,8 @@ from scipy.spatial import Delaunay from discretize.base import BaseMesh +from ..typing import RandomSeed + def add_block(cell_centers, model, p0, p1, prop_value): """Add a homogeneous block to an existing cell centered model @@ -414,15 +417,24 @@ def create_layers_model(cell_centers, layer_tops, layer_values): return model -def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None): - """Create random model by convolving a kernel with a uniformly distributed random model. +def create_random_model( + shape, + seed: RandomSeed | None = 1000, + anisotropy=None, + its=100, + bounds=None, +): + """ + Create random model by convolving a kernel with a uniformly distributed random model. Parameters ---------- shape : int or tuple of int Shape of the model. Can define a vector of size (n_cells) or define the dimensions of a tensor - seed : int, optional - If not None, sets the seed for the random uniform model that is convolved with the kernel. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for random uniform model that is convolved with the kernel. + It can either be an int, a predefined Numpy random number generator, or + any valid input to ``numpy.random.default_rng``. anisotropy : numpy.ndarray this is the (*3*, *n*) blurring kernel that is used. its : int @@ -450,14 +462,11 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) if bounds is None: bounds = [0, 1] - if seed is not None: - np.random.seed(seed) - print("Using a seed of: ", seed) - - if isinstance(shape, (int, float)): + if isinstance(shape, int): shape = (shape,) # make it a tuple for consistency - mr = np.random.rand(*shape) + rng = np.random.default_rng(seed=seed) + mr = rng.random(size=shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index aa83489187..f6450eb586 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -397,6 +397,38 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method +class TestSeedProperty: + """ + Test ``seed`` setter methods of directives. + """ + + directive_classes = ( + directives.AlphasSmoothEstimate_ByEig, + directives.BetaEstimate_ByEig, + directives.BetaEstimateMaxDerivative, + directives.ScalingMultipleDataMisfits_ByEig, + ) + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize( + "seed", + (42, np.random.default_rng(seed=1), np.array([1, 2])), + ids=("int", "rng", "array"), + ) + def test_valid_seed(self, directive_class, seed): + "Test if seed setter works as expected on valid seed arguments." + directive = directive_class(seed=seed) + assert directive.seed is seed + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize("seed", (42.1, np.array([1.0, 2.0]))) + def test_invalid_seed(self, directive_class, seed): + "Test if seed setter works as expected on valid seed arguments." + msg = "Unable to initialize the random number generator with " + with pytest.raises(TypeError, match=msg): + directive_class(seed=seed) + + class TestBetaEstimatorArguments: """ Test if arguments are assigned in beta estimator directives. diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 3c8e1db1fd..5d85f51804 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -79,7 +79,7 @@ def test_dm_eigenvalue_by_power_iteration(self): field = self.dmis.simulation.fields(self.true_model) max_eigenvalue_numpy, _ = eigsh(dmis_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmis, self.true_model, fields_list=field, n_pw_iter=30 + self.dmis, self.true_model, fields_list=field, n_pw_iter=30, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -92,7 +92,7 @@ def test_dm_eigenvalue_by_power_iteration(self): dmiscombo_matrix = 2 * self.G.T.dot(WtW.dot(self.G)) max_eigenvalue_numpy, _ = eigsh(dmiscombo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmiscombo, self.true_model, n_pw_iter=30 + self.dmiscombo, self.true_model, n_pw_iter=30, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -102,7 +102,7 @@ def test_reg_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) max_eigenvalue_numpy, _ = eigsh(reg_maxtrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.reg, self.true_model, n_pw_iter=100 + self.reg, self.true_model, n_pw_iter=100, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -114,7 +114,7 @@ def test_combo_eigenvalue_by_power_iteration(self): combo_matrix = dmis_matrix + self.beta * reg_maxtrix max_eigenvalue_numpy, _ = eigsh(combo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.mixcombo, self.true_model, n_pw_iter=100 + self.mixcombo, self.true_model, n_pw_iter=100, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) From 5ed93bf5d296c286506c2d73b3ca277d5a9a76b8 Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Thu, 30 May 2024 08:53:25 -0700 Subject: [PATCH 423/455] Extend docstrings for FDEM and TDEM fields and 3D simulations (#1414) Extend docstrings for the 3D field and simulation classes for FDEM and TDEM modules. Provide mathematical background for the implementations. Update format of docstrings following numpydoc style. Add a few constructor methods for some classes to fix some missing properties in child classes. --- .gitignore | 2 + simpeg/directives/directives.py | 3 +- .../frequency_domain/__init__.py | 22 +- .../frequency_domain/fields.py | 343 ++- .../frequency_domain/simulation.py | 1765 ++++++++++++--- .../electromagnetics/time_domain/__init__.py | 22 +- simpeg/electromagnetics/time_domain/fields.py | 361 +++- .../time_domain/simulation.py | 1883 +++++++++++++++-- simpeg/fields.py | 205 +- simpeg/regularization/base.py | 2 +- simpeg/utils/pgi_utils.py | 22 +- 11 files changed, 3902 insertions(+), 728 deletions(-) diff --git a/.gitignore b/.gitignore index c274ffb66f..5609580098 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,12 @@ docs/sg_execution_times.rst .vscode/* # paths to where data are downloaded +examples/04-dcip/test_url/* examples/20-published/bookpurnong_inversion/* examples/20-published/._bookpurnong_inversion examples/20-published/*.tar.gz examples/20-published/*.png +examples/20-published/Chile_GRAV_4_Miller/* tutorials/03-gravity/gravity/* tutorials/03-gravity/outputs/* tutorials/04-magnetics/magnetics/* diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 86d8728f4b..75ad4834ab 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -2527,8 +2527,7 @@ class UpdateSensitivityWeights(InversionDirective): The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity weights, thresholding is generally applied to set a minimum value. - Thresholding - ^^^^^^^^^^^^ + **Thresholding:** If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: diff --git a/simpeg/electromagnetics/frequency_domain/__init__.py b/simpeg/electromagnetics/frequency_domain/__init__.py index 5ec6ac0a13..e2f9422515 100644 --- a/simpeg/electromagnetics/frequency_domain/__init__.py +++ b/simpeg/electromagnetics/frequency_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Frequency-Domain EM (:mod:`simpeg.electromagnetics.frequency_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.frequency_domain -About ``frequency_domain`` +The ``frequency_domain`` module contains functionality for solving Maxwell's equations +in the frequency-domain for controlled sources. Where a :math:`+i\omega t` +Fourier convention is used, this module is used to solve problems of the form: + +.. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{J} = (\sigma + i \omega \varepsilon) \vec{E}` +* :math:`\vec{B} = \mu \vec{H}` + +and: + +* :math:`\vec{S}_m` represents a magnetic source term +* :math:`\vec{S}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py index ecf0023c55..429829b58e 100644 --- a/simpeg/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -6,34 +6,60 @@ class FieldsFDEM(Fields): - r""" - Fancy Field Storage for a FDEM survey. Only one field type is stored for - each problem, the rest are computed. The fields object acts like an array - and is indexed by + r"""Base class for storing FDEM fields. - .. code-block:: python + FDEM fields classes are used to store the discrete solution of the fields for a + corresponding FDEM simulation; see :class:`.BaseFDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, or ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + Parameters + ---------- + simulation : .BaseFDEMSimulation + The FDEM simulation object used to compute the discrete field solution. - If accessing all sources for a given field, use the :code:`:` + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: .. code-block:: python - f = problem.fields(m) + f = simulation.fields(m) e = f[:,'e'] b = f[:,'b'] - The array returned will be size (``nE`` or ``nF``, ``nSrcs`` :math:`\times` - ``nFrequencies``) + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ - knownFields = {} - _dtype = complex + def __init__(self, simulation): + dtype = complex + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): - """Grid location of the fieldType""" + """Return grid locations of the fieldType. + + Parameters + ---------- + fieldType : str + The field type. + + Returns + ------- + str + The grid locations. One of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + """ return self.aliasFields[fieldType][1] def _e(self, solution, source_list): @@ -295,28 +321,66 @@ def _jDeriv(self, src, du_dm_v, v, adjoint=False): class Fields3DElectricField(FieldsFDEM): - """ - Fields object for Simulation3DElectricField. + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.frequency_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'h'``, ``'b'``, ``'bPrimary'`` and ``'bSecondary'`` on mesh faces. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DElectricField + The FDEM simulation object associated with the fields. - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] """ - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "ePrimary": ["eSolution", "E", "_ePrimary"], - "eSecondary": ["eSolution", "E", "_eSecondary"], - "b": ["eSolution", "F", "_b"], - "bPrimary": ["eSolution", "F", "_bPrimary"], - "bSecondary": ["eSolution", "F", "_bSecondary"], - "j": ["eSolution", "E", "_j"], - "h": ["eSolution", "F", "_h"], - "charge": ["eSolution", "N", "_charge"], - "charge_density": ["eSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "ePrimary": ["eSolution", "E", "_ePrimary"], + "eSecondary": ["eSolution", "E", "_eSecondary"], + "b": ["eSolution", "F", "_b"], + "bPrimary": ["eSolution", "F", "_bPrimary"], + "bSecondary": ["eSolution", "F", "_bSecondary"], + "j": ["eSolution", "E", "_j"], + "h": ["eSolution", "F", "_h"], + "charge": ["eSolution", "N", "_charge"], + "charge_density": ["eSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._aveE2CCV = self.simulation.mesh.aveE2CCV self._aveF2CCV = self.simulation.mesh.aveF2CCV @@ -623,28 +687,67 @@ def _charge_density(self, eSolution, source_list): class Fields3DMagneticFluxDensity(FieldsFDEM): - """ - Fields object for Simulation3DMagneticFluxDensity. + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.frequency_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'bPrimary'``, ``'bSecondary'`` and ``'h'`` on mesh faces. + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticFluxDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e'] + b = f[source_list, 'b'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "bPrimary": ["bSolution", "F", "_bPrimary"], - "bSecondary": ["bSolution", "F", "_bSecondary"], - "e": ["bSolution", "E", "_e"], - "ePrimary": ["bSolution", "E", "_ePrimary"], - "eSecondary": ["bSolution", "E", "_eSecondary"], - "j": ["bSolution", "E", "_j"], - "h": ["bSolution", "F", "_h"], - "charge": ["bSolution", "N", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "bPrimary": ["bSolution", "F", "_bPrimary"], + "bSecondary": ["bSolution", "F", "_bSecondary"], + "e": ["bSolution", "E", "_e"], + "ePrimary": ["bSolution", "E", "_ePrimary"], + "eSecondary": ["bSolution", "E", "_eSecondary"], + "j": ["bSolution", "E", "_j"], + "h": ["bSolution", "F", "_h"], + "charge": ["bSolution", "N", "_charge"], + "charge_density": ["bSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -953,28 +1056,65 @@ def _charge_density(self, bSolution, source_list): class Fields3DCurrentDensity(FieldsFDEM): - """ - Fields object for Simulation3DCurrentDensity. + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.frequency_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DCurrentDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"jSolution": "F"} - aliasFields = { - "j": ["jSolution", "F", "_j"], - "jPrimary": ["jSolution", "F", "_jPrimary"], - "jSecondary": ["jSolution", "F", "_jSecondary"], - "h": ["jSolution", "E", "_h"], - "hPrimary": ["jSolution", "E", "_hPrimary"], - "hSecondary": ["jSolution", "E", "_hSecondary"], - "e": ["jSolution", "F", "_e"], - "b": ["jSolution", "E", "_b"], - "charge": ["bSolution", "CC", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "j": ["jSolution", "F", "_j"], + "jPrimary": ["jSolution", "F", "_jPrimary"], + "jSecondary": ["jSolution", "F", "_jSecondary"], + "h": ["jSolution", "E", "_h"], + "hPrimary": ["jSolution", "E", "_hPrimary"], + "hSecondary": ["jSolution", "E", "_hSecondary"], + "e": ["jSolution", "F", "_e"], + "b": ["jSolution", "E", "_b"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuI = self.simulation.MeMuI @@ -1344,28 +1484,65 @@ def _charge_density(self, jSolution, source_list): class Fields3DMagneticField(FieldsFDEM): - """ - Fields object for Simulation3DMagneticField. + r"""Fields class for storing 3D magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.frequency_domain.Simulation3DMagneticField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticField + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "hPrimary": ["hSolution", "E", "_hPrimary"], - "hSecondary": ["hSolution", "E", "_hSecondary"], - "j": ["hSolution", "F", "_j"], - "jPrimary": ["hSolution", "F", "_jPrimary"], - "jSecondary": ["hSolution", "F", "_jSecondary"], - "e": ["hSolution", "CCV", "_e"], - "b": ["hSolution", "CCV", "_b"], - "charge": ["hSolution", "CC", "_charge"], - "charge_density": ["hSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "hPrimary": ["hSolution", "E", "_hPrimary"], + "hSecondary": ["hSolution", "E", "_hSecondary"], + "j": ["hSolution", "F", "_j"], + "jPrimary": ["hSolution", "F", "_jPrimary"], + "jSecondary": ["hSolution", "F", "_jSecondary"], + "e": ["hSolution", "CCV", "_e"], + "b": ["hSolution", "CCV", "_b"], + "charge": ["hSolution", "CC", "_charge"], + "charge_density": ["hSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuDeriv = self.simulation.MeMuDeriv diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index 19619a8f89..de1afecb68 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -20,43 +20,51 @@ class BaseFDEMSimulation(BaseEMSimulation): - r""" - We start by looking at Maxwell's equations in the electric - field (:math:`\mathbf{e}`) and the magnetic flux - density (:math:`\mathbf{b}`) - - .. math :: - - \mathbf{C} \mathbf{e} + i \omega \mathbf{b} = \mathbf{s_m} - {\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e}} - - if using the E-B formulation (:code:`Simulation3DElectricField` - or :code:`Simulation3DMagneticFluxDensity`). Note that in this case, - :math:`\mathbf{s_e}` is an integrated quantity. - - If we write Maxwell's equations in terms of - :math:`\mathbf{h}` and current density :math:`\mathbf{j}`. - - .. math :: - - \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - i \omega \mathbf{M_{\mu}^e} \mathbf{h} = \mathbf{s_m} - \mathbf{C} \mathbf{h} - \mathbf{j} = \mathbf{s_e} - - if using the H-J formulation (:code:`Simulation3DCurrentDensity` or - :code:`Simulation3DMagneticField`). Note that here, :math:`\mathbf{s_m}` is an - integrated quantity. - - The problem performs the elimination so that we are solving the system - for :math:`mathbf{e}`, :math:`mathbf{b}`, :math:`mathbf{j}` or - :math:`mathbf{h}`. - + r"""Base finite volume FDEM simulation class. + + This class is used to define properties and methods necessary for solving + 3D frequency-domain EM problems. For a :math:`+i\omega t` Fourier convention, + Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{J} = \sigma \vec{E}` + * :math:`\vec{B} = \mu \vec{H}` + + and: + + * :math:`\vec{S}_m` represents a magnetic source term + * :math:`\vec{S}_e` represents a current source term + + Child classes of ``BaseFDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. """ fieldsPair = FieldsFDEM permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") def __init__( self, @@ -79,11 +87,12 @@ def __init__( @property def survey(self): - """The simulations survey. + """The FDEM survey object. Returns ------- - simpeg.electromagnetics.frequency_domain.survey.Survey + .frequency_domain.survey.Survey + The FDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -98,11 +107,12 @@ def survey(self, value): @property def storeJ(self): - """Whether to store the sensitivity matrix + """Whether to compute and store the sensitivity matrix. Returns ------- bool + Whether to compute and store the sensitivity matrix. """ return self._storeJ @@ -112,11 +122,16 @@ def storeJ(self, value): @property def forward_only(self): - """If True, A-inverse not stored at each frequency in forward simulation. + """Whether to store the factorizations of the inverses of the system matrices. + + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. Returns ------- bool + Whether to store the factorizations of the inverses of the system matrices. """ return self._forward_only @@ -154,12 +169,17 @@ def _get_edge_admittivity_property_matrix( # @profile def fields(self, m=None): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: numpy.ndarray - :return f: forward solution + Returns + ------- + .frequency_domain.fields.FieldsFDEM + The FDEM fields object. """ if m is not None: @@ -186,15 +206,34 @@ def fields(self, m=None): # @profile def Jvec(self, m, v, f=None): - """ - Sensitivity times a vector. - - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take sensitivity product with - (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + this method computes and returns the matrix-vector product: + + .. math:: + \mathbf{J v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -216,14 +255,34 @@ def Jvec(self, m, v, f=None): return Jv.dobs def Jtvec(self, m, v, f=None): - """ - Sensitivity transpose times a vector + r"""Compute the adjoint sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + this method computes and returns the matrix-vector product: - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take adjoint product with (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -263,13 +322,27 @@ def Jtvec(self, m, v, f=None): return mkvc(Jtv) def getJ(self, m, f=None): - """ - Method to form full J given a model m + r"""Generate the full sensitivity matrix. + + This method generates and stores the full sensitivity matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : .static.resistivity.fields.FieldsDC, optional + Fields solved for all sources. - :param numpy.ndarray m: inversion model (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: J (ndata, nP) + Returns + ------- + (n_data, n_param) numpy.ndarray + The full sensitivity matrix. """ self.model = m @@ -317,14 +390,32 @@ def getJ(self, m, f=None): return self._Jmatrix def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ + r"""Return the diagonal of :math:`\mathbf{J^T J}`. + + Where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters, + the sensitivity matrix :math:`\mathbf{J}` is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + This method returns the diagonals of :math:`\mathbf{J^T J}`. When the + *W* input argument is used to include a diagonal weighting matrix + :math:`\mathbf{W}`, this method returns the diagonal of + :math:`\mathbf{W^T J^T J W}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (n_param, n_param) scipy.sparse.csr_matrix + A diagonal weighting matrix. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray W: vector of weights (ndata,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: JtJ (nP,) + Returns + ------- + (n_param,) numpy.ndarray + The diagonals. """ self.model = m @@ -344,13 +435,33 @@ def getJtJdiag(self, m, W=None, f=None): # @profile def getSourceTerm(self, freq): - """ - Evaluates the sources for a given frequency and puts them in matrix - form + r"""Returns the discrete source terms for the frequency provided. + + This method computes and returns the discrete magnetic and electric source + terms for all soundings at the frequency provided. The exact shape and + implementation of the source terms when solving for the fields at each frequency + is formulation dependent. + + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.frequency_domain.Simulation3DElectricField` + * :class:`.frequency_domain.Simulation3DMagneticField` + * :class:`.frequency_domain.Simulation3DCurrentDensity` + * :class:`.frequency_domain.Simulation3DMagneticFluxDensity` - :param float freq: Frequency - :rtype: tuple - :return: (s_m, s_e) (nE or nF, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.get_sources_by_frequency(freq) n_fields = sum(src._fields_per_source for src in Srcs) @@ -376,6 +487,16 @@ def getSourceTerm(self, freq): @property def deleteTheseOnModelUpdate(self): + """List of model-dependent attributes to clean upon model update. + + Some of the FDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ toDelete = super().deleteTheseOnModelUpdate return toDelete + ["_Jmatrix", "_gtgdiag"] @@ -386,28 +507,99 @@ def deleteTheseOnModelUpdate(self): class Simulation3DElectricField(BaseFDEMSimulation): - r""" - By eliminating the magnetic flux density using - - .. math :: - - \mathbf{b} = \frac{1}{i \omega}\left(-\mathbf{C} \mathbf{e} + - \mathbf{s_m}\right) - - - we can write Maxwell's equations as a second order system in - :math:`mathbf{e}` only: + r"""3D FDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e \sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the electric field, we obtain: + + .. math:: + \mathbf{A \, e} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m }` - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} + - i \omega \mathbf{M^e_{\sigma}} \right)\mathbf{e} = - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M^e}\mathbf{s_e} - - which we solve for :math:`\mathbf{e}`. - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "eSolution" @@ -415,17 +607,30 @@ class Simulation3DElectricField(BaseFDEMSimulation): fieldsPair = Fields3DElectricField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - .. math :: + where - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} - + i \omega \mathbf{M^e_{\sigma}} + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -441,38 +646,98 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - conductivity model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\sigma}} = - i \omega \frac{d \mathbf{M^e_{\sigma}}(\mathbf{u})\mathbf{v} }{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dMe_dsig_v = self.MeSigmaDeriv(u, v, adjoint) return 1j * omega(freq) * dMe_dsig_v def getADeriv_mui(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of the system matrix with respect to the - permeability model and a vector. + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\mu^{-1}} = - \mathbf{C}^{\top} \frac{d \mathbf{M^f_{\mu^{-1}}}\mathbf{v}}{d\mathbf{m}} + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -483,6 +748,50 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return ( self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui(freq, u, v, adjoint) @@ -490,18 +799,32 @@ def getADeriv(self, freq, u, v, adjoint=False): ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - \mathbf{RHS} = \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M_e}\mathbf{s_e} + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -511,9 +834,49 @@ def getRHS(self, freq): return C.T * (MfMui * s_m) - 1j * omega(freq) * s_e def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the Right-hand side with respect to the model. This - includes calls to derivatives in the sources + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = -i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -534,26 +897,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`\mathbf{e}` using - - .. math :: - - \mathbf{e} = \mathbf{M^e_{\sigma}}^{-1} \left(\mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - \mathbf{s_e}\right) + r"""3D FDEM simulation in terms of the magnetic flux field. + + This simulation solves for the magnetic flux density at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic flux density, we obtain: + + .. math:: + \mathbf{A \, b} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m}` - and solve for :math:`\mathbf{b}` using: - - .. math :: - - \left(\mathbf{C} \mathbf{M^e_{\sigma}}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} + i \omega \right)\mathbf{b} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{M^e}\mathbf{s_e} - - .. note :: - The inverse problem will not work with full anisotropy - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "bSolution" @@ -561,17 +997,32 @@ class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): fieldsPair = Fields3DMagneticFluxDensity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\sigma}}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} + i \omega + Parameters + ---------- + freq : float + The frequency in Hz. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -592,23 +1043,51 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \frac{\mathbf{M^e_{\sigma}} \mathbf{v}}{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MfMui = self.MfMui @@ -625,6 +1104,52 @@ def getADeriv_sigma(self, freq, u, v, adjoint=False): # return C * (MeSigmaIDeriv * v) def getADeriv_mui(self, freq, u, v, adjoint=False): + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ MfMuiDeriv = self.MfMuiDeriv(u) MeSigmaI = self.MeSigmaI C = self.mesh.edge_curl @@ -634,6 +1159,52 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C * (MeSigmaI * (C.T * (MfMuiDeriv * v))) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint is True and self._makeASymmetric: v = self.MfMui * v @@ -647,17 +1218,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } - .. math :: + where - \mathbf{RHS} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{s_e} + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -679,15 +1265,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -721,30 +1341,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DCurrentDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{h}` using - - .. math :: - - \mathbf{h} = \frac{1}{i \omega} \mathbf{M_{\mu}^e}^{-1} - \left(-\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - \mathbf{M^e} \mathbf{s_m} \right) - - - and solve for :math:`mathbf{j}` using + r"""3D FDEM simulation in terms of the current density. + + This simulation solves for the current density at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the current density, we obtain: + + .. math:: + \mathbf{A \, j} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m}` - .. math :: - - \left(\mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} + i \omega\right)\mathbf{j} = - \mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{M^e} \mathbf{s_m} - - i\omega\mathbf{s_e} - - .. note:: - - This implementation does not yet work with full anisotropy!! - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "jSolution" @@ -760,17 +1449,31 @@ def __init__( self.permittivity = permittivity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\mu^{-1}}} - \mathbf{C}^{\top} \mathbf{M^f_{\sigma^{-1}}} + i\omega + Parameters + ---------- + freq : float + The frequency in Hz. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MeMuI = self.MeMuI @@ -791,28 +1494,49 @@ def getA(self, freq): return A def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - In this case, we assume that electrical conductivity, :math:`\sigma` - is the physical property of interest (i.e. :math:`\sigma` = - model.transform). Then we want - - .. math :: - - \frac{\mathbf{A(\sigma)} \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \mathbf{M^e_{mu^{-1}}} \mathbf{C^{\top}} - \frac{d \mathbf{M^f_{\sigma^{-1}}}\mathbf{v} }{d \mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\rho}` are the set of model parameters defining the resistivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MeMuI = self.MeMuI @@ -824,6 +1548,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C * (MeMuI * (C.T * (self.MfRhoDeriv(u, v, adjoint)))) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl MfRho = self.MfRho @@ -840,6 +1608,50 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return Aderiv def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint and self._makeASymmetric: v = self.MfRho * v @@ -853,17 +1665,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} - \mathbf{RHS} = \mathbf{C} \mathbf{M_{\mu}^e}^{-1}\mathbf{s_m} - - i\omega \mathbf{s_e} + where - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -878,15 +1705,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ # RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e @@ -923,22 +1784,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticField(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{j}` using - - .. math :: - - \mathbf{j} = \mathbf{C} \mathbf{h} - \mathbf{s_e} - - and solve for :math:`\mathbf{h}` using + r"""3D FDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic field, we obtain: + + .. math:: + \mathbf{A \, h} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}}` + * :math:`\mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m}` - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e}\right) \mathbf{h} = \mathbf{M^e} - \mathbf{s_m} + \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{s_e} - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "hSolution" @@ -946,19 +1884,31 @@ class Simulation3DMagneticField(BaseFDEMSimulation): fieldsPair = Fields3DMagneticField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e} + where + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MeMu = self.MeMu @@ -974,24 +1924,49 @@ def getA(self, freq): return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C}^{\top}\frac{d \mathbf{M^f_{\rho}}\mathbf{v}} - {d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl if adjoint: @@ -999,6 +1974,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ MeMuDeriv = self.MeMuDeriv(u) if adjoint is True: @@ -1007,23 +2026,82 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return 1j * omega(freq) * (MeMuDeriv * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( freq, u, v, adjoint ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where - .. math :: + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces - \mathbf{RHS} = \mathbf{M^e} \mathbf{s_m} + \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} \mathbf{s_e} + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -1039,15 +2117,50 @@ def getRHS(self, freq): return s_m + C.T * (Mfyhati * s_e) def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ _, s_e = src.eval(self) diff --git a/simpeg/electromagnetics/time_domain/__init__.py b/simpeg/electromagnetics/time_domain/__init__.py index 3fb8db7522..5891b847ce 100644 --- a/simpeg/electromagnetics/time_domain/__init__.py +++ b/simpeg/electromagnetics/time_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Time-Domain EM (:mod:`simpeg.electromagnetics.time_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.time_domain -About ``time_domain`` +The ``time_domain`` module contains functionality for solving Maxwell's equations +in the time-domain for controlled sources. Here, electric displacement is ignored, +and functionality is used to solve: + +.. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{j} = \sigma \vec{e}` +* :math:`\vec{b} = \mu \vec{h}` + +and: + +* :math:`\vec{s}_m` represents a magnetic source term +* :math:`\vec{s}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/time_domain/fields.py b/simpeg/electromagnetics/time_domain/fields.py index 384432c736..927a35e3b1 100644 --- a/simpeg/electromagnetics/time_domain/fields.py +++ b/simpeg/electromagnetics/time_domain/fields.py @@ -6,31 +6,48 @@ class FieldsTDEM(TimeFields): - r""" - Fancy Field Storage for a TDEM simulation. Only one field type is stored for - each problem, the rest are computed. The fields obejct acts like an array - and is indexed by + r"""Base class for storing TDEM fields. + + TDEM fields classes are used to store the discrete solution of the fields for a + corresponding TDEM simulation; see :class:`.time_domain.BaseTDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object used to compute the discrete field solution. + + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources + and all time steps: .. code-block:: python - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] - If accessing all sources for a given field, use the :code:`:` + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: .. code-block:: python - f = problem.fields(m) - e = f[:,'e'] - b = f[:,'b'] + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - The array returned will be size (nE or nF, nSrcs :math:`\times` - nFrequencies) """ - knownFields = {} - dtype = float + def __init__(self, simulation): + dtype = float + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): """Grid location of the fieldType""" @@ -86,49 +103,105 @@ def _jDeriv(self, tInd, src, dun_dm_v, v, adjoint=False): class FieldsDerivativesEB(FieldsTDEM): - """ - A fields object for satshing derivs in the EB formulation + r"""Field class for stashing derivatives for EB formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "F", - "eDeriv": "E", - "hDeriv": "F", - "jDeriv": "E", - "dbdtDeriv": "F", - "dhdtDeriv": "F", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "F", + "eDeriv": "E", + "hDeriv": "F", + "jDeriv": "E", + "dbdtDeriv": "F", + "dhdtDeriv": "F", + } class FieldsDerivativesHJ(FieldsTDEM): - """ - A fields object for satshing derivs in the HJ formulation + r"""Field class for stashing derivatives for HJ formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "E", - "eDeriv": "F", - "hDeriv": "E", - "jDeriv": "F", - "dbdtDeriv": "E", - "dhdtDeriv": "E", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "E", + "eDeriv": "F", + "hDeriv": "E", + "jDeriv": "F", + "dbdtDeriv": "E", + "dhdtDeriv": "E", + } class Fields3DMagneticFluxDensity(FieldsTDEM): - """Field Storage for a TDEM simulation.""" - - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "h": ["bSolution", "F", "_h"], - "e": ["bSolution", "E", "_e"], - "j": ["bSolution", "E", "_j"], - "dbdt": ["bSolution", "F", "_dbdt"], - "dhdt": ["bSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.time_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'h'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + * ``'e'`` and ``'j'`` on mesh edges. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticFluxDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "h": ["bSolution", "F", "_h"], + "e": ["bSolution", "E", "_e"], + "j": ["bSolution", "E", "_j"], + "dbdt": ["bSolution", "F", "_dbdt"], + "dhdt": ["bSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -273,19 +346,61 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DElectricField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "j": ["eSolution", "E", "_j"], - "b": ["eSolution", "F", "_b"], - # 'h': ['eSolution', 'F', '_h'], - "dbdt": ["eSolution", "F", "_dbdt"], - "dhdt": ["eSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'`` and ``'j'`` on mesh edges. + * ``'b'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DElectricField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and db/dt + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + dbdt = f[:, 'dbdt', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``dbdt`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + dbdt = f[source_list, 'dbdt', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "j": ["eSolution", "E", "_j"], + "b": ["eSolution", "F", "_b"], + # 'h': ['eSolution', 'F', '_h'], + "dbdt": ["eSolution", "F", "_dbdt"], + "dhdt": ["eSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -392,20 +507,63 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DMagneticField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "b": ["hSolution", "E", "_b"], - "dhdt": ["hSolution", "E", "_dhdt"], - "dbdt": ["hSolution", "E", "_dbdt"], - "j": ["hSolution", "F", "_j"], - "e": ["hSolution", "F", "_e"], - "charge": ["hSolution", "CC", "_charge"], - } + r"""Fields class for storing 3D total magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'b'``, ``'dbdt'`` and ``'dbdt'`` on mesh edges. + * ``'j'`` and ``'e'`` on mesh faces. + * ``'charge'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete magnetic fields and current density + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + h = f[:, 'h', :] + j = f[:, 'j', :] + + The array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + h = f[source_list, 'e', t_inds] + j = f[source_list, 'j', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "b": ["hSolution", "E", "_b"], + "dhdt": ["hSolution", "E", "_dhdt"], + "dbdt": ["hSolution", "E", "_dbdt"], + "j": ["hSolution", "F", "_j"], + "e": ["hSolution", "F", "_e"], + "charge": ["hSolution", "CC", "_charge"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI @@ -559,19 +717,62 @@ def _charge(self, hSolution, source_list, tInd): class Fields3DCurrentDensity(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"jSolution": "F"} - aliasFields = { - "dhdt": ["jSolution", "E", "_dhdt"], - "dbdt": ["jSolution", "E", "_dbdt"], - "j": ["jSolution", "F", "_j"], - "e": ["jSolution", "F", "_e"], - "charge": ["jSolution", "CC", "_charge"], - "charge_density": ["jSolution", "CC", "_charge_density"], - } + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.time_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'`` and ``'e'`` on mesh faces. + * ``'dbdt'`` and ``'dhdt'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DCurrentDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current densities and magnetic fields + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j', :] + h = f[:, 'h', :] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + And the array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j', t_inds] + h = f[source_list, 'h', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "dhdt": ["jSolution", "E", "_dhdt"], + "dbdt": ["jSolution", "E", "_dbdt"], + "j": ["jSolution", "F", "_j"], + "e": ["jSolution", "F", "_e"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index edb374cf06..8c5f23d006 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -17,10 +17,39 @@ class BaseTDEMSimulation(BaseTimeSimulation, BaseEMSimulation): - """ - We start with the first order form of Maxwell's equations, eliminate and - solve the second order form. For the time discretization, we use backward - Euler. + r"""Base class for quasi-static TDEM simulation with finite volume. + + This class is used to define properties and methods necessary for solving + 3D time-domain EM problems. In the quasi-static regime, we ignore electric + displacement, and Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{j} = \sigma \vec{e}` + * :math:`\vec{b} = \mu \vec{h}` + + and: + + * :math:`\vec{s}_m` represents a magnetic source term + * :math:`\vec{s}_e` represents a current source term + + Child classes of ``BaseTDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume and backward Euler time discretization. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. """ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @@ -34,10 +63,12 @@ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @property def survey(self): - """The survey for the simulation + """The TDEM survey object. + Returns ------- - simpeg.electromagnetics.time_domain.survey.Survey + .time_domain.survey.Survey + The TDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -51,14 +82,17 @@ def survey(self, value): @property def dt_threshold(self): - """The threshold used to determine if a previous matrix factor can be reused. + """Threshold used when determining the unique time-step lengths. - If the difference in time steps falls below this threshold, the factored matrix - is re-used. + The number of linear systems that must be factored to solve the forward + problem is equal to the number of unique time-step lengths. *dt_threshold* + effectively sets the round-off error when determining the unique time-step + lengths used by the simulation. Returns ------- float + Threshold used when determining the unique time-step lengths. """ return self._dt_threshold @@ -66,23 +100,18 @@ def dt_threshold(self): def dt_threshold(self, value): self._dt_threshold = validate_float("dt_threshold", value, min_val=0.0) - # def fields_nostore(self, m): - # """ - # Solve the forward problem without storing fields - - # :param numpy.ndarray m: inversion model (nP,) - # :rtype: numpy.ndarray - # :return numpy.ndarray: numpy.ndarray (nD,) - - # """ - def fields(self, m): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: simpeg.electromagnetics.time_domain.fields.FieldsTDEM - :return f: fields object + Returns + ------- + .time_domain.fields.FieldsTDEM + The TDEM fields object. """ self.model = m @@ -138,26 +167,34 @@ def fields(self, m): return f def Jvec(self, m, v, f=None): - r""" - Jvec computes the sensitivity times a vector + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: .. math:: - \mathbf{J} \mathbf{v} = - \frac{d\mathbf{P}}{d\mathbf{F}} - \left( - \frac{d\mathbf{F}}{d\mathbf{u}} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} - \right) - \mathbf{v} + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: - \mathbf{A} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial \mathbf{A} (\mathbf{u}, \mathbf{m})} - {\partial\mathbf{m}} = - \frac{d \mathbf{RHS}}{d \mathbf{m}} + \mathbf{J v} + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -168,7 +205,7 @@ def Jvec(self, m, v, f=None): # mat to store previous time-step's solution deriv times a vector for # each source - # size: nu x nSrc + # size: nu x n_sources # this is a bit silly @@ -248,27 +285,34 @@ def Jvec(self, m, v, f=None): return np.hstack(Jv) def Jtvec(self, m, v, f=None): - r""" - Jvec computes the adjoint of the sensitivity times a vector + r"""Compute the adjoint sensitivity matrix times a vector. - .. math:: + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: - \mathbf{J}^\top \mathbf{v} = - \left( - \frac{d\mathbf{u}}{d\mathbf{m}} ^ \top - \frac{d\mathbf{F}}{d\mathbf{u}} ^ \top - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} ^ \top - \right) - \frac{d\mathbf{P}}{d\mathbf{F}} ^ \top - \mathbf{v} + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. - \frac{d\mathbf{u}}{d\mathbf{m}} ^\top \mathbf{A}^\top + - \frac{d\mathbf{A}(\mathbf{u})}{d\mathbf{m}} ^ \top = - \frac{d \mathbf{RHS}}{d \mathbf{m}} ^ \top + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -392,9 +436,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getSourceTerm(self, tInd): - """ - Assemble the source term. This ensures that the RHS is a vector / array - of the correct size + r"""Return the discrete source terms for the time index provided. + + This method computes and returns the discrete magnetic and electric source terms for + all soundings at the time index provided. The exact shape and implementation of source + terms when solving for the fields at each time-step is formulation dependent. + + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.time_domain.Simulation3DElectricField` + * :class:`.time_domain.Simulation3DMagneticField` + * :class:`.time_domain.Simulation3DCurrentDensity` + * :class:`.time_domain.Simulation3DMagneticFluxDensity` + + Parameters + ---------- + tInd : int + The time index. Value between ``[0, n_steps]``. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.source_list @@ -414,8 +481,12 @@ def getSourceTerm(self, tInd): return s_m, s_e def getInitialFields(self): - """ - Ask the sources for initial fields + """Returns the fields for all sources at the initial time. + + Returns + ------- + (n_edges or n_faces, n_sources) numpy.ndarray + The fields for all sources at the initial time. """ Srcs = self.survey.source_list @@ -436,6 +507,40 @@ def getInitialFields(self): return ifields def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): + r"""Derivative of the initial fields with respect to the model for a given source. + + For a given source object `src`, let :math:`\mathbf{u_0}` represent the initial + fields discretized to the mesh. Where :math:`\mathbf{m}` are the model parameters + and :math:`\mathbf{v}` is a vector, this method computes and returns: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}} \, \mathbf{v} + + or the adjoint operation: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + src : .time_domain.sources.BaseTDEMSrc + A TDEM source. + v : numpy.ndarray + A vector of appropriate dimension. When `adjoint` is ``False``, `v` is a + (n_param,) numpy.ndarray. When `adjoint` is ``True``, `v` is a (n_edges or n_faces,) + numpy.ndarray. + adjoint : bool + Whether to perform the adjoint operation. + f : .time_domain.fields.BaseTDEMFields, optional + The TDEM fields object. + + Returns + ------- + numpy.ndarray + Derivatives of the intial fields with respect to the model for a given source. + (n_edges or n_faces,) numpy.ndarray when `adjoint` is ``False``. (n_param,) numpy.ndarray + when `ajoint` is ``True``. + """ ifieldsDeriv = mkvc( getattr(src, "{}InitialDeriv".format(self._fieldType), None)( self, v, adjoint, f @@ -462,6 +567,33 @@ def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): # initial condition @property def Adcinv(self): + r"""Inverse of the factored system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + This property is used to compute and store the inverse of the factored linear system + matrix for the DC resistivity problem given by: + + .. math:: + \mathbf{A_{dc}} \, \boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the system matrix, :math:`\boldsymbol{\phi_0}` represents the + discrete solution for the electric potential and :math:`\mathbf{q_{dc}}` is the discrete + right-hand side. Electric fields are computed by applying a discrete gradient operator + to the discrete electric potential solution. + + Returns + ------- + pymatsolver.solvers.Base + Inver of the factored systems matrix for the DC resistivity problem. + + Notes + ----- + See the docstrings for :class:`.resistivity.BaseDCSimulation`, + :class:`.resistivity.Simulation3DCellCentered` and + :class:`.resistivity.Simulation3DNodal` to learn + more about how the DC resistivity problem is solved. + """ if not hasattr(self, "getAdc"): raise NotImplementedError( "Support for galvanic sources has not been implemented for " @@ -476,6 +608,16 @@ def Adcinv(self): @property def clean_on_model_update(self): + """List of model-dependent attributes to clean upon model update. + + Some of the TDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ items = super().clean_on_model_update return items + ["_Adcinv"] #: clear DC matrix factors on any model updates @@ -490,83 +632,160 @@ def clean_on_model_update(self): class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): - r""" - Starting from the quasi-static E-B formulation of Maxwell's equations - (semi-discretized) + r"""3D TDEM simulation in terms of the magnetic flux density. + + This simulation solves for the magnetic flux density at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e - \mathbf{C} \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{s_m} \\ - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e} - - - where :math:`\mathbf{s_e}` is an integrated quantity, we eliminate - :math:`\mathbf{e}` using + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} - \mathbf{e} = \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - - to obtain a second order semi-discretized system in :math:`\mathbf{b}` + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - and moving everything except the time derivative to the rhs gives + By cancelling like-terms and combining the discrete expressions in terms of the magnetic flux density, we obtain: .. math:: - \frac{\partial \mathbf{b}}{\partial t} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}} b} + + \frac{\partial \mathbf{b}}{\partial t} + = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s_e} + - \frac{\partial \mathbf{s_m}}{\partial t} - For the time discretization, we use backward euler. To solve for the - :math:`n+1` th time step, we have + Finally, we discretize in time according to backward Euler. The discrete magnetic flux density + on mesh faces at time :math:`t_k > t_0` is obtained by solving the following at each time-step: .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q_k} - \mathbf{B}_k \mathbf{b}_{k-1} - \frac{\mathbf{b}^{n+1} - \mathbf{b}^{n}}{\mathbf{dt}} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b}^{n+1} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e}^{n+1} + - \mathbf{s_m}^{n+1} + where :math:`\Delta t_k = t_k - t_{k-1}` and + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] - re-arranging to put :math:`\mathbf{b}^{n+1}` on the left hand side gives + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: .. math:: - - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) \mathbf{b}^{n+1} = - \mathbf{b}^{n} + \mathbf{dt}(\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{s_e}^{n+1} + \mathbf{s_m}^{n+1}) + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{b_1} \\ \mathbf{b_2} \\ \vdots \\ \mathbf{b_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 b_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic flux densities at the initial time :math:`\mathbf{b_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "b" _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity #: A simpeg.EM.TDEM.Fields3DMagneticFluxDensity object + fieldsPair = Fields3DMagneticFluxDensity Fields_Derivs = FieldsDerivativesEB def getAdiag(self, tInd): - r""" - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -583,8 +802,52 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of ADiag + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{b_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -605,8 +868,27 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ dt = self.time_steps[tInd] @@ -619,11 +901,82 @@ def getAsubdiag(self, tInd): return Asubdiag def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} = \mathbf{0} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} = \mathbf{0} + + The derivative operation returns a vector of zeros because the sub-diagonal system matrix + does not depend on the model!!! + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{b_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() * v def getRHS(self, tInd): - """ - Assemble the RHS + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ C = self.mesh.edge_curl MeSigmaI = self.MeSigmaI @@ -637,8 +990,51 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): - """ - Derivative of the RHS + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -673,38 +1069,124 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # ------------------------------- Simulation3DElectricField ------------------------------- # class Simulation3DElectricField(BaseTDEMSimulation): - r""" - Solve the EB-formulation of Maxwell's equations for the electric field, e. - - Starting with + r"""3D TDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: - \nabla \times \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = \mathbf{s_m} \ - \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} = \mathbf{s_e} + .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - we eliminate :math:`\frac{\partial b}{\partial t}` using + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \frac{\partial \mathbf{b}}{\partial t} = - \nabla \times \mathbf{e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - taking the time-derivative of Ampere's law, we see + By cancelling like-terms and combining the discrete expressions in terms of the electric field, we obtain: .. math:: + \mathbf{C^T M_{f\frac{1}{\mu}} C e} + \mathbf{M_{e\sigma}}\frac{\partial \mathbf{e}}{\partial t} + = \mathbf{C^T M_{f\frac{1}{\mu}}} \frac{\partial \mathbf{s_m}}{\partial t} + - \frac{\partial \mathbf{s_e}}{\partial t} - \frac{\partial}{\partial t}\left( \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} \right) = \frac{\partial \mathbf{s_e}}{\partial t} \ - \nabla \times \mu^{-1} \frac{\partial \mathbf{b}}{\partial t} - \sigma \frac{\partial\mathbf{e}}{\partial t} = \frac{\partial \mathbf{s_e}}{\partial t} + Finally, we discretize in time according to backward Euler. The discrete electric fields + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{b}_{k-1} - which gives us + where :math:`\Delta t_k = t_k - t_{k-1}` and .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{q}_k = \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] - \nabla \times \mu^{-1} \nabla \times \mathbf{e} + \sigma \frac{\partial\mathbf{e}}{\partial t} = \nabla \times \mu^{-1} \mathbf{s_m} + \frac{\partial \mathbf{s_e}}{\partial t} + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{e_1} \\ \mathbf{e_2} \\ \vdots \\ \mathbf{e_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 e_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the electric fields at the initial time :math:`\mathbf{e_0}` + are computed analytically or numerically depending on whether the + source is galvanic and carries non-zero current at the initial time. """ @@ -715,10 +1197,7 @@ class Simulation3DElectricField(BaseTDEMSimulation): # @profile def Jtvec(self, m, v, f=None): - """ - Jvec computes the adjoint of the sensitivity times a vector - """ - + # Doctring inherited from parent class. if f is None: f = self.fields(m) @@ -873,8 +1352,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getAdiag(self, tInd): - """ - Diagonal of the system matrix at a given time index + r"""Diagonal system matrix for the time-step index provided. + + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -886,8 +1389,52 @@ def getAdiag(self, tInd): return C.T.tocsr() * (MfMui * C) + 1.0 / dt * MeSigma def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Deriv of ADiag with respect to electrical conductivity + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the specified time-step; + i.e. :math:`\mathbf{e_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ assert tInd >= 0 and tInd < self.nT @@ -900,8 +1447,28 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -910,9 +1477,48 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeSigma def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of the matrix below the diagonal with respect to electrical - conductivity + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{e_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dt = self.time_steps[tInd] @@ -922,8 +1528,36 @@ def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getRHS(self, tInd): - """ - right hand side + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ # Omit this: Note input was tInd+1 # if tInd == len(self.time_steps): @@ -936,68 +1570,268 @@ def getRHS(self, tInd): return -1.0 / dt * (s_e - s_en1) + self.mesh.edge_curl.T * self.MfMui * s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): - # right now, we are assuming that s_e, s_m do not depend on the model. - return Zero() - - def getAdc(self): - MeSigma = self.MeSigma - Grad = self.mesh.nodal_gradient - Adc = Grad.T.tocsr() * MeSigma * Grad - # Handling Null space of A - Adc[0, 0] = Adc[0, 0] + 1.0 - return Adc - - def getAdcDeriv(self, u, v, adjoint=False): - Grad = self.mesh.nodal_gradient - if not adjoint: - return Grad.T * self.MeSigmaDeriv(-u, v, adjoint) - else: - return self.MeSigmaDeriv(-u, Grad * v, adjoint) - - # def clean(self): - # """ - # Clean factors - # """ - # if self.Adcinv is not None: - # self.Adcinv.clean() - + r"""Derivative of the right-hand side times a vector for a given source and time index. -############################################################################### -# # -# H-J Formulation # -# # -############################################################################### + The right-hand side for a given source at time index *k* is constructed according to: -# ------------------------------- Simulation3DMagneticField ------------------------------- # + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}} } + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + where -class Simulation3DMagneticField(BaseTDEMSimulation): - r""" - Solve the H-J formulation of Maxwell's equations for the magnetic field h. + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - We start with Maxwell's equations in terms of the magnetic field and - current density + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - .. math:: + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns - \nabla \times \rho \mathbf{j} + \mu \frac{\partial h}{\partial t} = \mathbf{s_m} \ - \nabla \times \mathbf{h} - \mathbf{j} = \mathbf{s_e} + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + Or the adjoint operation - and eliminate :math:`\mathbf{j}` using + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ + # right now, we are assuming that s_e, s_m do not depend on the model. + return Zero() + + def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the nodal formulation, i.e.: + + .. math:: + \mathbf{A_{dc}} = \mathbf{G^T \, M_{e\sigma} \, G} + + where :math:`\mathbf{G}` is the nodal gradient operator with imposed boundary conditions, + and :math:`\mathbf{M_{e\sigma}}` is the inner product matrix for conductivities projected to edges. + + The electric fields at the initial time :math:`\mathbf{e_0}` are obtained by applying the + nodal gradient operator. I.e.: + + .. math:: + \mathbf{e_0} = \mathbf{G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DNodal` + for a full description of the nodal DC resistivity formulation. + + Returns + ------- + (n_nodes, n_nodes) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ + MeSigma = self.MeSigma + Grad = self.mesh.nodal_gradient + Adc = Grad.T.tocsr() * MeSigma * Grad + # Handling Null space of A + Adc[0, 0] = Adc[0, 0] + 1.0 + return Adc + + def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_nodes,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at nodes. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_nodes,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_nodes,) for the standard operation. + (n_param,) for the adjoint operation. + """ + Grad = self.mesh.nodal_gradient + if not adjoint: + return Grad.T * self.MeSigmaDeriv(-u, v, adjoint) + else: + return self.MeSigmaDeriv(-u, Grad * v, adjoint) + + +############################################################################### +# # +# H-J Formulation # +# # +############################################################################### + +# ------------------------------- Simulation3DMagneticField ------------------------------- # + + +class Simulation3DMagneticField(BaseTDEMSimulation): + r"""3D TDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current density is defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: + + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} - \mathbf{j} = \nabla \times \mathbf{h} - \mathbf{s_e} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - giving + Cancelling like-terms and combining the discrete expressions in terms of the magnetic field, we obtain: .. math:: + \mathbf{C^T M_{f\rho} C \, h} + \mathbf{M_{e\mu}} \frac{\partial \mathbf{h}}{\partial t} + = \mathbf{C^T M_{f\rho} s_e} - \frac{\partial \mathbf{s_m}}{\partial t} - \nabla \times \rho \nabla \times \mathbf{h} + \mu \frac{\partial h}{\partial t} - = \nabla \times \rho \mathbf{s_e} + \mathbf{s_m} + Finally, we discretize in time according to backward Euler. The discrete magnetic field + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{h}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{h}_{k-1} + + where :math:`\Delta t_k = t_k - t_{k-1}` and + + .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}}\\ + &\mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{h_1} \\ \mathbf{h_2} \\ \vdots \\ \mathbf{h_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 h_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic fields at the initial time :math:`\mathbf{h_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ @@ -1007,9 +1841,32 @@ class Simulation3DMagneticField(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1021,6 +1878,52 @@ def getAdiag(self, tInd): return C.T * (MfRho * C) + 1.0 / dt * MeMu def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{h_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1031,6 +1934,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT dt = self.time_steps[tInd] @@ -1038,9 +1964,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeMu def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{h_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. + """ C = self.mesh.edge_curl MfRho = self.MfRho s_m, s_e = self.getSourceTerm(tInd) @@ -1048,6 +2046,52 @@ def getRHS(self, tInd): return C.T * (MfRho * s_e) + s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl s_m, s_e = src.eval(self, self.times[tInd]) @@ -1056,13 +2100,79 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # assumes no source derivs return C.T * self.MfRhoDeriv(s_e, v, adjoint) + # I DON'T THINK THIS IS CURRENTLY USED BY THE H-FORMULATION. def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + See the *Notes* section of the doc strings for + :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T @@ -1077,12 +2187,124 @@ def getAdcDeriv(self, u, v, adjoint=False): class Simulation3DCurrentDensity(BaseTDEMSimulation): - r""" - Solve the H-J formulation for current density + r"""3D TDEM simulation in terms of the current density. + + This simulation solves for the current density at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: + + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + + Cancelling like-terms and combining the discrete expressions in terms of the current density, we obtain: + + .. math:: + \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} \, j} + + \frac{\partial \mathbf{j}}{\partial t} = + - \frac{\partial \mathbf{s_e}}{\partial t} + - \mathbf{C M_{e\mu}^{-1}} \frac{\partial \mathbf{s_m}}{\partial t} + + Finally, we discretize in time according to backward Euler. The discrete current density + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + + .. math:: + \mathbf{A}_k \mathbf{j}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{j}_{k-1} + + where :math:`\Delta t_k = t_k - t_{k-1}` and - In this case, we eliminate :math:`\partial \mathbf{h} / \partial t` and - solve for :math:`\mathbf{j}` + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = - \frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{j_1} \\ \mathbf{j_2} \\ \vdots \\ \mathbf{j_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 j_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the current densities at the initial time :math:`\mathbf{j_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "j" @@ -1091,9 +2313,33 @@ class Simulation3DCurrentDensity(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1111,6 +2357,53 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{j_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1128,6 +2421,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT eye = sp.eye(self.mesh.n_faces) @@ -1138,9 +2454,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * eye def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{j_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. + """ if tInd == len(self.time_steps): tInd = tInd - 1 @@ -1156,15 +2544,130 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() # assumes no derivs on sources def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + The current density at the initial time :math:`\mathbf{j_0}` are obtained by applying: + + .. math:: + \mathbf{j_0} = \mathbf{M_{f\rho}^{-1} \, G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T diff --git a/simpeg/fields.py b/simpeg/fields.py index d3eb461c27..fd20be1716 100644 --- a/simpeg/fields.py +++ b/simpeg/fields.py @@ -5,24 +5,71 @@ class Fields: - """Fancy Field Storage + r"""Base class for storing fields. + + Fields classes are used to store the discrete field solution for a + corresponding simulation object; see :py:class:`SimPEG.simulation.BaseSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type (``'b'``) that lives on mesh faces (``'F'``) + from the E-field solution (``'eSolution'``) by calling a method (``'_b'``). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, + ``{'eSolution': complex, 'bSolution': complex}``. Examples -------- - >>> fields = Fields( - ... simulation=simulation, knownFields={"phi": "CC"} - ... ) - >>> fields[:,'phi'] = phi + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:,'e'] + b = f[:,'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ _dtype = float _knownFields = {} _aliasFields = {} - def __init__( - self, simulation, knownFields=None, aliasFields=None, dtype=None, **kwargs - ): - super().__init__(**kwargs) + def __init__(self, simulation, knownFields=None, aliasFields=None, dtype=None): self.simulation = simulation if knownFields is not None: @@ -45,11 +92,12 @@ def __init__( @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- - simpeg.simulation.BaseSimulation + SimPEG.simulation.BaseSimulation + The simulation object used to compute the field solution. """ return self._simulation @@ -61,39 +109,41 @@ def simulation(self, value): @property def knownFields(self): - """The known fields of this object. - - The dictionary representing the known fields and their locations on the simulation - mesh. The keys are the names of the fields, and the values are the location on - the mesh. + """The field solutions and where they are discretized on the mesh. - >>> fields.knownFields - {'e': 'E', 'phi': 'CC'} + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. The ``key`` defines the name + of the field solution that is stored, and a ``str`` defines where + on the mesh the stored field solution is discretized. The + ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would define the `eSolution` on edges and `bSolution` on faces. Returns ------- dict - They keys are the field names and the values are the field locations. + The keys are the field solution names and the values {'N', 'CC', 'E'. 'F'} + define where the field solution is discretized. """ return self._knownFields @property def aliasFields(self): - """The aliased fields of this object. + """The aliased fields of the object. - The dictionary representing the aliased fields that can be accessed on this - object. The keys are the names of the fields, and the values are a list of the - known field, the aliased field's location on the mesh, and a function that goes - from the known field to the aliased field. + Aliases are defined to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: - >>> fields.aliasFields - {'b': ['e', 'F', '_e']} + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). Returns ------- @@ -106,28 +156,53 @@ def aliasFields(self): @property def dtype(self): - """The data type of the storage matrix + """Python data type(s) used to store the fields. + + the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. Returns ------- dtype or dict of {str : dtype} + Python data type(s) used to store the fields. """ return self._dtype @property def mesh(self): + """Mesh used by the simulation. + + Returns + ------- + discretize.BaseMesh + Mesh used by the simulation. + """ return self.simulation.mesh @property def survey(self): + """Survey used by the simulation. + + Returns + ------- + SimPEG.survey.BaseSurvey + Survey used by the simulation. + """ return self.simulation.survey def startup(self): + """Run startup to connect the simulation's discrete attributes to the fields object.""" pass @property def approxSize(self): - """The approximate cost to storing all of the known fields.""" + """Approximate cost of storing all of the known fields in MB. + + Returns + ------- + int + Approximate cost of storing all of the known fields in MB. + """ sz = 0.0 for f in self.knownFields: loc = self.knownFields[f] @@ -273,21 +348,73 @@ def __contains__(self, other): class TimeFields(Fields): - """Fancy Field Storage for time domain problems - .. code:: python + r"""Base class for storing TDEM fields. + + ``TimeFields`` is a base class for storing discrete field solutions for simulations + that use discrete time-stepping; see :py:class:`SimPEG.simulation.BaseTimeSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseTimeSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. + + Examples + -------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`, `n_steps`). We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - fields = TimeFields(simulation=simulation, knownFields={'phi':'CC'}) - fields[:,'phi', timeInd] = phi - print(fields[src0,'phi']) """ @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- - simpeg.simulation.BaseTimeSimulation + SimPEG.simulation.BaseTimeSimulation + The simulation object used to compute the field solution. """ return self._simulation diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 823c7fd0de..3165244228 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -311,7 +311,7 @@ def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. Parameters - ------------ + ---------- key: str Name of the weights requested. diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index a11fe9fb56..7141fccc98 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -158,13 +158,13 @@ def compute_clusters_covariances(self): def order_clusters_GM_weight(self, outputindex=False): """Order clusters by decreasing weights - PARAMETERS + Parameters ---------- outputindex : bool, default: ``True`` If ``True``, return the sorting index - RETURN - ------ + Returns + ------- np.ndarray Sorting index """ @@ -194,6 +194,7 @@ def _check_weights(self, weights, n_components, n_samples): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Check the user provided 'weights'. + Parameters ---------- weights : array-like, shape (n_components,) or (n_samples, n_components) @@ -271,6 +272,7 @@ def _initialize_parameters(self, X, random_state): """ [modified from Scikit-Learn.mixture._base] Initialize the model parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -303,6 +305,7 @@ def _m_step(self, X, log_resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] M step. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -327,6 +330,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the tied covariance matrix. + Parameters ---------- resp : array-like, shape (n_samples, n_components) @@ -334,6 +338,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): nk : array-like, shape (n_components,) means : array-like, shape (n_components, n_features) reg_covar : float + Returns ------- covariance : array, shape (n_features, n_features) @@ -350,6 +355,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the Gaussian distribution parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -360,6 +366,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ The regularization added to the diagonal of the covariance matrices. covariance_type : {'full', 'tied', 'diag', 'spherical'} The type of precision matrices. + Returns ------- nk : array-like, shape (n_components,) @@ -385,9 +392,11 @@ def _e_step(self, X): """ [modified from Scikit-Learn.mixture.gaussian_mixture] E step. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- log_prob_norm : float @@ -426,6 +435,7 @@ def _estimate_log_gaussian_prob_with_sensW( """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_log_gaussian_prob] Estimate the log Gaussian probability with depth or sensitivity weighting. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -438,6 +448,7 @@ def _estimate_log_gaussian_prob_with_sensW( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) @@ -484,9 +495,11 @@ def _estimate_weighted_log_prob_with_sensW(self, X, sensW): """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_weighted_log_prob] Estimate the weighted log-probabilities, log P(X | Z) + log weights. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- weighted_log_prob : array, shape (n_samples, n_component) @@ -1254,6 +1267,7 @@ def _initialize(self, X, resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Initialization of the Gaussian mixture parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1295,6 +1309,7 @@ def _estimate_log_gaussian_prob( """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the log Gaussian probability. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1306,6 +1321,7 @@ def _estimate_log_gaussian_prob( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) From 5d489f3d854ac27a0fa3528090160539fb895038 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 31 May 2024 10:41:36 -0700 Subject: [PATCH 424/455] Magnetic simulation with Choclo as engine (#1321) Add new implementation of the magnetic simulation using the integral formulation based on [Choclo](https://www.fatiando.org/choclo) and Numba. Allow users to run the simulation with Choclo as engine through a new `engine` argument of the constructor of the simulation class. Add `numba_parallel` argument to control if the simulation should be run in parallel or in a single thread. Move common methods and properties of the gravity and magnetic simulation to the base class for potential field simulations. Add new `potential_fields/_numba_utils.py` file with functions that are needed both in the magnetic and gravity Numba implementations. Refactor and extend tests for the magnetic simulation, including tests that assert the accuracy of the new implementation against analytic solutions. Add new tests for the base class for potential field simulations. Update examples using Choclo as engine, include admonitions instructing how to use it. --------- Co-authored-by: Jacob Edman Co-authored-by: Joseph Capriotti --- simpeg/potential_fields/_numba_utils.py | 43 + simpeg/potential_fields/base.py | 137 ++- .../gravity/_numba_functions.py | 68 +- simpeg/potential_fields/gravity/simulation.py | 110 +- .../magnetics/_numba_functions.py | 659 +++++++++++ .../potential_fields/magnetics/simulation.py | 243 +++- tests/pf/test_base_pf_simulation.py | 306 +++++ tests/pf/test_forward_Grav_Linear.py | 5 +- tests/pf/test_forward_Mag_Linear.py | 1049 +++++++++++------ .../04-magnetics/plot_2a_magnetics_induced.py | 14 + .../04-magnetics/plot_2b_magnetics_mvi.py | 5 + .../plot_inv_2a_magnetics_induced.py | 12 + 12 files changed, 2118 insertions(+), 533 deletions(-) create mode 100644 simpeg/potential_fields/_numba_utils.py create mode 100644 simpeg/potential_fields/magnetics/_numba_functions.py create mode 100644 tests/pf/test_base_pf_simulation.py diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py new file mode 100644 index 0000000000..2bdea6da2a --- /dev/null +++ b/simpeg/potential_fields/_numba_utils.py @@ -0,0 +1,43 @@ +""" +Utility functions for Numba implementations + +These functions are meant to be used both in the Numba-based gravity and +magnetic simulations. +""" + +try: + from numba import jit +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + +@jit(nopython=True) +def kernels_in_nodes_to_cell(kernels, nodes_indices): + """ + Evaluate integral on a given cell from evaluation of kernels on nodes + + Parameters + ---------- + kernels : (n_active_nodes,) array + Array with kernel values on each one of the nodes in the mesh. + nodes_indices : (8,) array of int + Indices of the nodes for the current cell in "F" order (x changes + faster than y, and y faster than z). + + Returns + ------- + float + """ + result = ( + -kernels[nodes_indices[0]] + + kernels[nodes_indices[1]] + + kernels[nodes_indices[2]] + - kernels[nodes_indices[3]] + + kernels[nodes_indices[4]] + - kernels[nodes_indices[5]] + - kernels[nodes_indices[6]] + + kernels[nodes_indices[7]] + ) + return result diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index d457483e3e..db63463a1a 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -10,6 +10,11 @@ from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string +try: + import choclo +except ImportError: + choclo = None + ############################################################################### # # # Base Potential Fields Simulation # @@ -47,7 +52,14 @@ class BasePFSimulation(LinearSimulation): n_processes : None or int, optional The number of processes to use in the internal multiprocessing pool for forward modeling. The default value of 1 will not use multiprocessing. Any other setting - will. `None` implies setting by the number of cpus. + will. `None` implies setting by the number of cpus. If engine is + ``"choclo"``, then this argument will be ignored. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. Notes ----- @@ -73,6 +85,8 @@ def __init__( store_sensitivities="ram", n_processes=1, sensitivity_dtype=np.float32, + engine="geoana", + numba_parallel=True, **kwargs, ): # If deprecated property set with kwargs @@ -88,10 +102,18 @@ def __init__( self.store_sensitivities = store_sensitivities self.sensitivity_dtype = sensitivity_dtype + self.engine = engine + self.numba_parallel = numba_parallel super().__init__(mesh, **kwargs) self.solver = None self.n_processes = n_processes + # Check sensitivity_path when engine is "choclo" + self._check_engine_and_sensitivity_path() + + # Check dimensions of the mesh when engine is "choclo" + self._check_engine_and_mesh_dimensions() + # Find non-zero cells indices if ind_active is None: ind_active = np.ones(mesh.n_cells, dtype=bool) @@ -188,6 +210,54 @@ def n_processes(self, value): value = validate_integer("n_processes", value, min_val=1) self._n_processes = value + @property + def engine(self) -> str: + """ + Engine that will be used to run the simulation. + + It can be either ``"geoana"`` or "``choclo``". + """ + return self._engine + + @engine.setter + def engine(self, value: str): + validate_string( + "engine", value, string_list=("geoana", "choclo"), case_sensitive=True + ) + if value == "choclo" and choclo is None: + raise ImportError( + "The choclo package couldn't be found." + "Running a gravity simulation with 'engine=\"choclo\"' needs " + "choclo to be installed." + "\nTry installing choclo with:" + "\n pip install choclo" + "\nor:" + "\n conda install choclo" + ) + self._engine = value + + @property + def numba_parallel(self) -> bool: + """ + Run simulation in parallel or single-threaded when using Numba. + + If True, the simulation will run in parallel. If False, it will + run in serial. + + .. important:: + + If ``engine`` is not ``"choclo"`` this property will be ignored. + """ + return self._numba_parallel + + @numba_parallel.setter + def numba_parallel(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"Invalid 'numba_parallel' value of type {type(value)}. Must be a bool." + ) + self._numba_parallel = value + @property def ind_active(self): """Active topography cells. @@ -255,6 +325,71 @@ def linear_operator(self): np.save(sens_name, kernel) return kernel + def _check_engine_and_sensitivity_path(self): + """ + Check if sensitivity_path is a file if engine is set to "choclo" + """ + if ( + self.engine == "choclo" + and self.store_sensitivities == "disk" + and os.path.isdir(self.sensitivity_path) + ): + raise ValueError( + f"The passed sensitivity_path '{self.sensitivity_path}' is " + "a directory. " + "When using 'choclo' as the engine, 'senstivity_path' " + "should be the path to a new or existing file." + ) + + def _check_engine_and_mesh_dimensions(self): + """ + Check dimensions of the mesh when using choclo as engine + """ + if self.engine == "choclo" and self.mesh.dim != 3: + raise ValueError( + f"Invalid mesh with {self.mesh.dim} dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + + def _get_active_nodes(self): + """ + Return locations of nodes only for active cells + + Also return an array containing the indices of the "active nodes" for + each active cell in the mesh + """ + # Get all nodes in the mesh + if isinstance(self.mesh, discretize.TreeMesh): + nodes = self.mesh.total_nodes + elif isinstance(self.mesh, discretize.TensorMesh): + nodes = self.mesh.nodes + else: + raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") + # Get original cell_nodes but only for active cells + cell_nodes = self.mesh.cell_nodes + # If all cells in the mesh are active, return nodes and cell_nodes + if self.nC == self.mesh.n_cells: + return nodes, cell_nodes + # Keep only the cell_nodes for active cells + cell_nodes = cell_nodes[self.ind_active] + # Get the unique indices of the nodes that belong to every active cell + # (these indices correspond to the original `nodes` array) + unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) + # Select only the nodes that belong to the active cells (active nodes) + active_nodes = nodes[unique_nodes] + # Reshape indices of active cell nodes for each active cell in the mesh + active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) + return active_nodes, active_cell_nodes + + def _get_components_and_receivers(self): + """Generator for receiver locations and their field components.""" + if not hasattr(self.survey, "source_field"): + raise AttributeError( + f"The survey '{self.survey}' has no 'source_field' attribute." + ) + for receiver_object in self.survey.source_field.receiver_list: + yield receiver_object.components, receiver_object.locations + class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """Base equivalent source layer simulation class. diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py index 1d6b363b27..3eb6ae9bf1 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -15,6 +15,8 @@ def jit(*args, **kwargs): else: from numba import jit, prange +from .._numba_utils import kernels_in_nodes_to_cell + def _forward_gravity( receivers, @@ -30,7 +32,7 @@ def _forward_gravity( This function should be used with a `numba.jit` decorator, for example: - ..code:: + .. code:: from numba import jit @@ -85,16 +87,9 @@ def _forward_gravity( fields[i] += ( constant_factor * densities[k] - * _kernels_in_nodes_to_cell( + * kernels_in_nodes_to_cell( kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], + cell_nodes[k, :], ) ) @@ -112,7 +107,7 @@ def _sensitivity_gravity( This function should be used with a `numba.jit` decorator, for example: - ..code:: + .. code:: from numba import jit @@ -162,16 +157,9 @@ def _sensitivity_gravity( ) # Compute sensitivity matrix elements from the kernel values for k in range(n_cells): - sensitivity_matrix[i, k] = constant_factor * _kernels_in_nodes_to_cell( + sensitivity_matrix[i, k] = constant_factor * kernels_in_nodes_to_cell( kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], + cell_nodes[k, :], ) @@ -204,46 +192,6 @@ def _evaluate_kernel( return kernel_func(dx, dy, dz, distance) -@jit(nopython=True) -def _kernels_in_nodes_to_cell( - kernels, - nodes_indices_0, - nodes_indices_1, - nodes_indices_2, - nodes_indices_3, - nodes_indices_4, - nodes_indices_5, - nodes_indices_6, - nodes_indices_7, -): - """ - Evaluate integral on a given cell from evaluation of kernels on nodes - - Parameters - ---------- - kernels : (n_active_nodes,) numpy.ndarray - Array with kernel values on each one of the nodes in the mesh. - nodes_indices : ints - Indices of the nodes for the current cell in "F" order (x changes - faster than y, and y faster than z). - - Returns - ------- - float - """ - result = ( - -kernels[nodes_indices_0] - + kernels[nodes_indices_1] - + kernels[nodes_indices_2] - - kernels[nodes_indices_3] - + kernels[nodes_indices_4] - - kernels[nodes_indices_5] - - kernels[nodes_indices_6] - + kernels[nodes_indices_7] - ) - return result - - # Define decorated versions of these functions _sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) _sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 311b19dfc5..1596b15ec2 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -1,7 +1,5 @@ -import os import warnings import numpy as np -import discretize import scipy.constants as constants from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG @@ -85,13 +83,13 @@ class Simulation3DIntegral(BasePFSimulation): Gravity survey with information of the receivers. ind_active : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. - rho : numpy.ndarray (optional) + rho : numpy.ndarray, optional Density array for the active cells in the mesh. - rhoMap : Mapping (optional) + rhoMap : Mapping, optional Model mapping. sensitivity_dtype : numpy.dtype, optional Data type that will be used to build the sensitivity matrix. - store_sensitivities : str + store_sensitivities : {"ram", "disk", "forward_only"} Options for storing sensitivity matrix. There are 3 options - 'ram': sensitivities are stored in the computer's RAM @@ -102,9 +100,8 @@ class Simulation3DIntegral(BasePFSimulation): sensitivity_path : str, optional Path to store the sensitivity matrix if ``store_sensitivities`` is set to ``"disk"``. Default to "./sensitivities". - engine : str, optional - Choose which engine should be used to run the forward model: - ``"geoana"`` or "``choclo``". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. numba_parallel : bool, optional If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be @@ -122,51 +119,13 @@ def __init__( numba_parallel=True, **kwargs, ): - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.rho = rho self.rhoMap = rhoMap self._G = None self._gtg_diagonal = None self.modelMap = self.rhoMap - self.numba_parallel = numba_parallel - self.engine = engine - self._sanity_checks_engine(kwargs) - if self.engine == "choclo": - # Check dimensions of the mesh - if self.mesh.dim != 3: - raise ValueError( - f"Invalid mesh with {self.mesh.dim} dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - # Define jit functions - if numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_parallel - self._forward_gravity = _forward_gravity_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_serial - self._forward_gravity = _forward_gravity_serial - - def _sanity_checks_engine(self, kwargs): - """ - Sanity checks for the engine parameter. - Needs the kwargs passed to the __init__ method to raise some warnings. - Will set n_processes to None if it's present in kwargs. - """ - if self.engine not in ("choclo", "geoana"): - raise ValueError( - f"Invalid engine '{self.engine}'. Choose from 'geoana' or 'choclo'." - ) - if self.engine == "choclo" and choclo is None: - raise ImportError( - "The choclo package couldn't be found." - "Running a gravity simulation with 'engine=\"choclo\"' needs " - "choclo to be installed." - "\nTry installing choclo with:" - "\n pip install choclo" - "\nor:" - "\n conda install choclo" - ) # Warn if n_processes has been passed if self.engine == "choclo" and "n_processes" in kwargs: warnings.warn( @@ -176,15 +135,15 @@ def _sanity_checks_engine(self, kwargs): stacklevel=1, ) self.n_processes = None - # Sanity checks for sensitivity_path when using choclo and storing in disk - if self.engine == "choclo" and self.store_sensitivities == "disk": - if os.path.isdir(self.sensitivity_path): - raise ValueError( - f"The passed sensitivity_path '{self.sensitivity_path}' is " - "a directory. " - "When using 'choclo' as the engine, 'senstivity_path' " - "should be the path to a new or existing file." - ) + + # Define jit functions + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_gravity = _sensitivity_gravity_parallel + self._forward_gravity = _forward_gravity_parallel + else: + self._sensitivity_gravity = _sensitivity_gravity_serial + self._forward_gravity = _forward_gravity_serial def fields(self, m): """ @@ -455,45 +414,6 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix - def _get_active_nodes(self): - """ - Return locations of nodes only for active cells - - Also return an array containing the indices of the "active nodes" for - each active cell in the mesh - """ - # Get all nodes in the mesh - if isinstance(self.mesh, discretize.TreeMesh): - nodes = self.mesh.total_nodes - elif isinstance(self.mesh, discretize.TensorMesh): - nodes = self.mesh.nodes - else: - raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") - # Get original cell_nodes but only for active cells - cell_nodes = self.mesh.cell_nodes - # If all cells in the mesh are active, return nodes and cell_nodes - if self.nC == self.mesh.n_cells: - return nodes, cell_nodes - # Keep only the cell_nodes for active cells - cell_nodes = cell_nodes[self.ind_active] - # Get the unique indices of the nodes that belong to every active cell - # (these indices correspond to the original `nodes` array) - unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) - # Select only the nodes that belong to the active cells (active nodes) - active_nodes = nodes[unique_nodes] - # Reshape indices of active cell nodes for each active cell in the mesh - active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) - return active_nodes, active_cell_nodes - - def _get_components_and_receivers(self): - """Generator for receiver locations and their field components.""" - if not hasattr(self.survey, "source_field"): - raise AttributeError( - f"The survey '{self.survey}' has no 'source_field' attribute." - ) - for receiver_object in self.survey.source_field.receiver_list: - yield receiver_object.components, receiver_object.locations - class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py new file mode 100644 index 0000000000..92a5d2eacd --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -0,0 +1,659 @@ +""" +Numba functions for magnetic simulation of rectangular prisms +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from .._numba_utils import kernels_in_nodes_to_cell + + +def _sensitivity_mag( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_mag = jit(nopython=True, parallel=True)(_sensitivity_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _forward_mag( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + """ + Forward model single magnetic component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the forward will be computing assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computing assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + ux * model[k] + + uy * model[k + n_cells] + + uz * model[k + 2 * n_cells] + ) + ) + + +def _forward_tmi( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + """ + Forward model the TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + bx * model[k] + + by * model[k + n_cells] + + bz * model[k + 2 * n_cells] + ) + ) + + +_sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) +_sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) +_forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) +_forward_tmi_parallel = jit(nopython=True, parallel=True)(_forward_tmi) +_forward_mag_serial = jit(nopython=True, parallel=False)(_forward_mag) +_forward_mag_parallel = jit(nopython=True, parallel=True)(_forward_mag) +_sensitivity_mag_serial = jit(nopython=True, parallel=False)(_sensitivity_mag) +_sensitivity_mag_parallel = jit(nopython=True, parallel=True)(_sensitivity_mag) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 5d3d27171c..85d6e53109 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1,3 +1,4 @@ +import warnings import numpy as np import scipy.sparse as sp from geoana.kernels import ( @@ -20,11 +21,68 @@ from .analytics import CongruousMagBC from .survey import Survey +from ._numba_functions import ( + choclo, + _sensitivity_tmi_parallel, + _sensitivity_tmi_serial, + _sensitivity_mag_parallel, + _sensitivity_mag_serial, + _forward_tmi_parallel, + _forward_tmi_serial, + _forward_mag_parallel, + _forward_mag_serial, +) + +if choclo is not None: + CHOCLO_SUPPORTED_COMPONENTS = {"tmi", "bx", "by", "bz"} + CHOCLO_KERNELS = { + "bx": (choclo.prism.kernel_ee, choclo.prism.kernel_en, choclo.prism.kernel_eu), + "by": (choclo.prism.kernel_en, choclo.prism.kernel_nn, choclo.prism.kernel_nu), + "bz": (choclo.prism.kernel_eu, choclo.prism.kernel_nu, choclo.prism.kernel_uu), + } + class Simulation3DIntegral(BasePFSimulation): """ - magnetic simulation in integral form. + Magnetic simulation in integral form. + Parameters + ---------- + mesh : discretize.TreeMesh or discretize.TensorMesh + Mesh use to run the magnetic simulation. + survey : simpeg.potential_fields.magnetics.Survey + Magnetic survey with information of the receivers. + ind_active : (n_cells) numpy.ndarray, optional + Array that indicates which cells in ``mesh`` are active cells. + chi : numpy.ndarray, optional + Susceptibility array for the active cells in the mesh. + chiMap : Mapping, optional + Model mapping. + model_type : str, optional + Whether the model are susceptibilities of the cells (``"scalar"``), + or effective susceptibilities (``"vector"``). + is_amplitude_data : bool, optional + If True, the returned fields will be the amplitude of the magnetic + field. If False, the fields will be returned unmodified. + sensitivity_dtype : numpy.dtype, optional + Data type that will be used to build the sensitivity matrix. + store_sensitivities : {"ram", "disk", "forward_only"} + Options for storing sensitivity matrix. There are 3 options + + - 'ram': sensitivities are stored in the computer's RAM + - 'disk': sensitivities are written to a directory + - 'forward_only': you intend only do perform a forward simulation and + sensitivities do not need to be stored + + sensitivity_path : str, optional + Path to store the sensitivity matrix if ``store_sensitivities`` is set + to ``"disk"``. Default to "./sensitivities". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ chi, chiMap, chiDeriv = props.Invertible("Magnetic Susceptibility (SI)") @@ -36,10 +94,12 @@ def __init__( chiMap=None, model_type="scalar", is_amplitude_data=False, - **kwargs + engine="geoana", + numba_parallel=True, + **kwargs, ): self.model_type = model_type - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.chi = chi self.chiMap = chiMap @@ -49,6 +109,28 @@ def __init__( self.is_amplitude_data = is_amplitude_data self.modelMap = self.chiMap + # Warn if n_processes has been passed + if self.engine == "choclo" and "n_processes" in kwargs: + warnings.warn( + "The 'n_processes' will be ignored when selecting 'choclo' as the " + "engine in the magnetic simulation.", + UserWarning, + stacklevel=1, + ) + self.n_processes = None + + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_tmi = _sensitivity_tmi_parallel + self._sensitivity_mag = _sensitivity_mag_parallel + self._forward_tmi = _forward_tmi_parallel + self._forward_mag = _forward_mag_parallel + else: + self._sensitivity_tmi = _sensitivity_tmi_serial + self._sensitivity_mag = _sensitivity_mag_serial + self._forward_tmi = _forward_tmi_serial + self._forward_mag = _forward_mag_serial + @property def model_type(self): """Type of magnetization model @@ -103,7 +185,10 @@ def fields(self, model): self.model = model # model = self.chiMap * model if self.store_sensitivities == "forward_only": - fields = mkvc(self.linear_operator()) + if self.engine == "choclo": + fields = self._forward(self.chi) + else: + fields = mkvc(self.linear_operator()) else: fields = np.asarray( self.G @ self.chi.astype(self.sensitivity_dtype, copy=False) @@ -117,7 +202,10 @@ def fields(self, model): @property def G(self): if getattr(self, "_G", None) is None: - self._G = self.linear_operator() + if self.engine == "choclo": + self._G = self._sensitivity_matrix() + else: + self._G = self.linear_operator() return self._G @@ -494,6 +582,151 @@ def deleteTheseOnModelUpdate(self): deletes = deletes + ["_gtg_diagonal", "_ampDeriv"] return deletes + def _forward(self, model): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``model_type`` is ``"scalar"``, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``model_type`` is + ``"vector"``, and the array should have ``3 * n_active_cells`` + elements. + + Returns + ------- + (nD, ) array + Always return a ``np.float64`` array. + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start computing the fields + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._forward_tmi( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._forward_mag( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) array + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate sensitivity matrix + if self.model_type == "scalar": + n_columns = self.nC + else: + n_columns = 3 * self.nC + shape = (self.survey.nD, n_columns) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start filling the sensitivity matrix + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._sensitivity_tmi( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._sensitivity_mag( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return sensitivity_matrix + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py new file mode 100644 index 0000000000..9cf7964ee4 --- /dev/null +++ b/tests/pf/test_base_pf_simulation.py @@ -0,0 +1,306 @@ +""" +Test BasePFSimulation class +""" + +import pytest +import numpy as np +from discretize import CylindricalMesh, TensorMesh, TreeMesh + +import simpeg +from simpeg.potential_fields.base import BasePFSimulation +from simpeg.survey import BaseSurvey +from simpeg.potential_fields import gravity, magnetics + + +@pytest.fixture +def mock_simulation_class(): + """ + Mock simulation class as child of BasePFSimulation + """ + + class MockSimulation(BasePFSimulation): + @property + def G(self): + """Define a dummy G property to avoid warnings on tests.""" + pass + + return MockSimulation + + +@pytest.fixture +def tensor_mesh(): + """ + Return sample TensorMesh + """ + h = (3, 3, 3) + return TensorMesh(h) + + +@pytest.fixture +def tree_mesh(): + """ + Return sample TensorMesh + """ + h = (4, 4, 4) + mesh = TreeMesh(h) + mesh.refine_points(points=(0, 0, 0), level=2) + return mesh + + +@pytest.fixture +def mock_survey_class(): + """ + Mock survey class as child of BaseSurvey + """ + + class MockSurvey(BaseSurvey): + pass + + return MockSurvey + + +class TestEngine: + """ + Test the engine property and some of its relations with other attributes + """ + + def test_invalid_engine(self, tensor_mesh, mock_simulation_class): + """ + Test if error is raised after invalid engine + """ + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_invalid_engine_without_choclo( + self, tensor_mesh, mock_simulation_class, monkeypatch + ): + """ + Test error after choosing "choclo" as engine but not being installed + """ + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + engine = "choclo" + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_sensitivity_path_as_dir(self, tensor_mesh, mock_simulation_class, tmpdir): + """ + Test error if the sensitivity_path is a dir + + Error should be raised if using ``engine=="choclo"`` and setting + ``store_sensitivities="disk"``. + """ + sensitivity_path = str(tmpdir.mkdir("sensitivities")) + msg = f"The passed sensitivity_path '{sensitivity_path}' is a directory." + with pytest.raises(ValueError, match=msg): + mock_simulation_class( + tensor_mesh, + engine="choclo", + store_sensitivities="disk", + sensitivity_path=sensitivity_path, + ) + + +class TestGetActiveNodes: + """ + Tests _get_active_nodes private method + """ + + def test_invalid_mesh(self, tensor_mesh, mock_simulation_class): + """ + Test error on invalid mesh class + """ + # Initialize base simulation with valid mesh (so we don't trigger + # errors in the constructor) + simulation = mock_simulation_class(tensor_mesh) + # Assign an invalid mesh to the simulation + simulation.mesh = CylindricalMesh(tensor_mesh.h) + msg = "Invalid mesh of type CylindricalMesh." + with pytest.raises(TypeError, match=msg): + simulation._get_active_nodes() + + def test_no_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tensor mesh + """ + simulation = mock_simulation_class(tensor_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tensor_mesh.nodes) + np.testing.assert_equal(active_cell_nodes, tensor_mesh.cell_nodes) + + def test_no_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tree mesh + """ + simulation = mock_simulation_class(tree_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tree_mesh.total_nodes) + np.testing.assert_equal(active_cell_nodes, tree_mesh.cell_nodes) + + def test_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tensor_mesh.n_cells, dtype=bool) + active_cells[0] = True + # Initialize simulation + simulation = mock_simulation_class(tensor_mesh, ind_active=active_cells) + # Build expected active_nodes and active_cell_nodes + expected_active_nodes = tensor_mesh.nodes[tensor_mesh[0].nodes] + expected_active_cell_nodes = np.atleast_2d(np.arange(8, dtype=int)) + # Test method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, expected_active_nodes) + np.testing.assert_equal(active_cell_nodes, expected_active_cell_nodes) + + def test_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tree_mesh.n_cells, dtype=bool) + active_cells[0] = True + + # Initialize simulation + simulation = mock_simulation_class(tree_mesh, ind_active=active_cells) + + # Build expected active_nodes (in the right order for a single cell) + expected_active_nodes = [ + [0, 0, 0], + [0.25, 0, 0], + [0, 0.25, 0], + [0.25, 0.25, 0], + [0, 0, 0.25], + [0.25, 0, 0.25], + [0, 0.25, 0.25], + [0.25, 0.25, 0.25], + ] + + # Run method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + + # Check shape of active nodes and check if all of them are there + assert active_nodes.shape == (8, 3) + for node in expected_active_nodes: + assert node in active_nodes + + # Check shape of active_cell_nodes and check if they are in the right + # order + assert active_cell_nodes.shape == (1, 8) + for node, node_index in zip(expected_active_nodes, active_cell_nodes[0]): + np.testing.assert_equal(node, active_nodes[node_index]) + + +class TestGetComponentsAndReceivers: + """ + Test _get_components_and_receivers private method + """ + + @pytest.fixture + def receiver_locations(self): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + return receiver_locations + + @pytest.fixture + def gravity_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["gxy", "guv"] + receivers = gravity.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + return gravity.Survey(source_field) + + @pytest.fixture + def magnetic_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["tmi", "bx"] + receivers = magnetics.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=45.0, + declination=12.0, + ) + return magnetics.Survey(source_field) + + def test_missing_source_field( + self, tensor_mesh, mock_survey_class, mock_simulation_class + ): + """ + Test error after missing survey in simulation + """ + survey = mock_survey_class(source_list=None) + simulation = mock_simulation_class(tensor_mesh, survey=survey) + msg = "The survey '(.*)' has no 'source_field' attribute." + with pytest.raises(AttributeError, match=msg): + # need to iterate over the generator to actually test its code + [item for item in simulation._get_components_and_receivers()] + + def test_components_and_receivers_gravity( + self, tensor_mesh, gravity_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a gravity survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=gravity_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["gxy", "guv"] + np.testing.assert_equal(receivers, receiver_locations) + + def test_components_and_receivers_magnetics( + self, tensor_mesh, magnetic_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a magnetic survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=magnetic_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["tmi", "bx"] + np.testing.assert_equal(receivers, receiver_locations) + + +class TestInvalidMeshChoclo: + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = TensorMesh(h, "CC") + else: + mesh = TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh, mock_simulation_class): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + with pytest.raises(ValueError, match=msg): + mock_simulation_class(mesh, engine="choclo") diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 40aff12b53..32a964710a 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -329,7 +329,8 @@ def test_invalid_sensitivity_dtype_assignment(self, simple_mesh, invalid_dtype): def test_invalid_engine(self, simple_mesh): """Test if error is raised after invalid engine.""" engine = "invalid engine" - with pytest.raises(ValueError, match=f"Invalid engine '{engine}'"): + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral(simple_mesh, engine=engine) def test_choclo_and_n_proceesses(self, simple_mesh): @@ -405,7 +406,7 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): Check if error is raised when choclo is missing and chosen as engine. """ # Monkeypatch choclo in simpeg.potential_fields.base - monkeypatch.setattr(simpeg.potential_fields.gravity.simulation, "choclo", None) + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) # Check if error is raised msg = "The choclo package couldn't be found." with pytest.raises(ImportError, match=msg): diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 45ff17ba8c..9cf3498238 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,120 +1,667 @@ -import pytest -import unittest +from __future__ import annotations import discretize import numpy as np +import pytest from geoana.em.static import MagneticPrism from scipy.constants import mu_0 +import simpeg from simpeg import maps, utils from simpeg.potential_fields import magnetics as mag -def test_ana_mag_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") +def get_block_inds(grid: np.ndarray, block: np.ndarray) -> np.ndarray: + """ + Get the indices for a block + + Parameters + ---------- + grid : np.ndarray + (n, 3) array of xyz locations + block : np.ndarray + (3, 2) array of (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of + the block. + + Returns + ------- + np.ndarray + boolean array of indices corresponding to the block + """ + + return np.where( + (grid[:, 0] > block[0, 0]) + & (grid[:, 0] < block[0, 1]) + & (grid[:, 1] > block[1, 0]) + & (grid[:, 1] < block[1, 1]) + & (grid[:, 2] > block[2, 0]) + & (grid[:, 2] < block[2, 1]) + ) - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) +def create_block_model( + mesh: discretize.TensorMesh, + blocks: tuple[np.ndarray, ...], + block_params: tuple[float, ...] | tuple[np.ndarray, ...], +) -> tuple[np.ndarray, np.ndarray]: + """ + Create a magnetic model from a sequence of blocks + + Parameters + ---------- + mesh : discretize.TensorMesh + TensorMesh object to put the model on + blocks : Tuple[np.ndarray, ...] + Tuple of block definitions (each element is (3, 2) array of + (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of the block) + block_params : Tuple[float, ...] + Tuple of parameters to assign for each block. Must be the same length + as ``blocks``. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of the magnetic model and active_cells (a boolean array) + + Raises + ------ + ValueError + if ``blocks`` and ``block_params`` have incompatible dimensions + """ + if len(blocks) != len(block_params): + raise ValueError( + "'blocks' and 'block_params' must have the same number of elements" + ) + model = np.zeros((mesh.n_cells, np.atleast_1d(block_params[0]).shape[0])) + for block, params in zip(blocks, block_params): + block_ind = get_block_inds(mesh.cell_centers, block) + model[block_ind] = params + active_cells = np.any(np.abs(model) > 0, axis=1) + return model.squeeze(), active_cells + + +def create_mag_survey( + components: list[str], + receiver_locations: np.ndarray, + inducing_field_params: tuple[float, float, float], +) -> mag.Survey: + """ + create a magnetic Survey + + Parameters + ---------- + components : List[str] + List of components to model + receiver_locations : np.ndarray + (n, 3) array of xyz receiver locations + inducing_field_params : Tuple[float, float, float] + amplitude, inclination, and declination of the inducing field + + Returns + ------- + mag.Survey + a magnetic Survey instance + """ + + receivers = mag.Point(receiver_locations, components=components) + strenght, inclination, declination = inducing_field_params + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=strenght, + inclination=inclination, + declination=declination, + ) + return mag.Survey(source_field) + + +class TestsMagSimulation: + """ + Test mag simulation against the analytic solutions single prisms + """ + + @pytest.fixture + def mag_mesh(self) -> discretize.TensorMesh: + """ + a small tensor mesh for testing magnetic simulations + + Returns + ------- + discretize.TensorMesh + the tensor mesh for testing + """ + # Define a mesh + cs = 0.2 + hxind = [(cs, 41)] + hyind = [(cs, 41)] + hzind = [(cs, 41)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + return mesh + + @pytest.fixture + def two_blocks(self) -> tuple[np.ndarray, np.ndarray]: + """ + The parameters defining two blocks + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of (3, 2) arrays of (xmin, xmax), (ymin, ymax), (zmin, zmax) + dimensions of each block. + """ + block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) + block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) + return block1, block2 + + @pytest.fixture + def receiver_locations(self) -> np.ndarray: + """ + a grid of receivers for testing + + Returns + ------- + np.ndarray + (n, 3) array of receiver locations + """ + # Create plane of observations + nx, ny = 5, 5 + xr = np.linspace(-20, 20, nx) + yr = np.linspace(-20, 20, ny) + X, Y = np.meshgrid(xr, yr) + Z = np.ones_like(X) * 3.0 + return np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] + + @pytest.fixture + def inducing_field( + self, + ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + """ + inducing field + + Return inducing field as amplitude and angles and as vector components. + + Returns + ------- + tuple[tuple[float, float, float], tuple[float, float, float]] + (amplitude, inclination, declination), (b_x, b_y, b_z) + """ + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) + return (h0_amplitude, h0_inclination, h0_declination), b0 + + @pytest.mark.parametrize( + "engine,parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_and_tmi_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test forwarding the magnetic field and tmi (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, ) - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) - active_cells = model != 0.0 - model_reduced = model[active_cells] + data = sim.dpred(model_reduced) + d_x = data[0::4] + d_y = data[1::4] + d_z = data[2::4] + d_t = data[3::4] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # TMI projection + tmi = sim.tmi_projection + d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] + + # Check results + rtol, atol = 1e-7, 1e-6 + np.testing.assert_allclose( + d_t, d_t2, rtol=rtol, atol=atol + ) # double check internal projection + np.testing.assert_allclose(d_x, d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_y, d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_z, d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_t, d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_gradiometry_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic gradiometry components (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bxx", "bxy", "bxz", "byy", "byz", "bzz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + if engine == "choclo": + # gradient simulation not implemented for choclo yet + with pytest.raises(NotImplementedError): + data = sim.dpred(model_reduced) + else: + data = sim.dpred(model_reduced) + d_xx = data[0::6] + d_xy = data[1::6] + d_xz = data[2::6] + d_yy = data[3::6] + d_yz = data[4::6] + d_zz = data[5::6] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + + prism_3.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_vector_and_tmi_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic vector and TMI (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) + data = sim.dpred(model_reduced).reshape(-1, 4) - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) + tmi = sim.tmi_projection + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data[:, 0], d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 1], d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 2], d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 3], d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_amplitude_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic field amplitude (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - data = sim.dpred(model_reduced) - d_x = data[0::4] - d_y = data[1::4] - d_z = data[2::4] - d_t = data[3::4] - - tmi = sim.tmi_projection - d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] - np.testing.assert_allclose(d_t, d_t2) # double check internal projection + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + is_amplitude_data=True, + engine=engine, + **parallel_kwargs, + ) - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + data = sim.dpred(model_reduced) - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - np.testing.assert_allclose(d_x, d[:, 0]) - np.testing.assert_allclose(d_y, d[:, 1]) - np.testing.assert_allclose(d_z, d[:, 2]) - np.testing.assert_allclose(d_t, d @ tmi) + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) + d_amp = np.linalg.norm(d, axis=1) + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data, d_amp, rtol=rtol, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_sensitivity_dtype( + self, + engine, + store_sensitivities, + mag_mesh, + receiver_locations, + tmp_path, + ): + """Test sensitivity_dtype.""" + # Create survey + receivers = mag.Point(receiver_locations, components="tmi") + sources = mag.UniformBackgroundField( + [receivers], amplitude=50_000, inclination=45, declination=10 + ) + survey = mag.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(mag_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=mag_mesh.n_cells) + # Create simulation + sensitivity_path = tmp_path + if engine == "choclo": + sensitivity_path /= "dummy" + simulation = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=idenMap, + ind_active=active_cells, + engine=engine, + store_sensitivities=store_sensitivities, + sensitivity_path=str(sensitivity_path), + ) + # sensitivity_dtype should be float64 when running forward only, + # but float32 in other cases + if store_sensitivities == "forward_only": + assert simulation.sensitivity_dtype is np.float64 + else: + assert simulation.sensitivity_dtype is np.float32 + + @pytest.mark.parametrize("invalid_dtype", (float, np.float16)) + def test_invalid_sensitivity_dtype_assignment(self, mag_mesh, invalid_dtype): + """ + Test invalid sensitivity_dtype assignment + """ + simulation = mag.Simulation3DIntegral(mag_mesh) + # Check if error is raised + msg = "sensitivity_dtype must be either np.float32 or np.float64." + with pytest.raises(TypeError, match=msg): + simulation.sensitivity_dtype = invalid_dtype + + def test_invalid_engine(self, mag_mesh): + """Test if error is raised after invalid engine.""" + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine=engine) + + def test_choclo_and_n_proceesses(self, mag_mesh): + """Check if warning is raised after passing n_processes with choclo engine.""" + msg = "The 'n_processes' will be ignored when selecting 'choclo'" + with pytest.warns(UserWarning, match=msg): + simulation = mag.Simulation3DIntegral( + mag_mesh, engine="choclo", n_processes=2 + ) + # Check if n_processes was overwritten and set to None + assert simulation.n_processes is None + + def test_choclo_and_sensitivity_path_as_dir(self, mag_mesh, tmp_path): + """ + Check if error is raised when sensitivity_path is a dir with choclo engine. + """ + # Create a sensitivity_path directory + sensitivity_path = tmp_path / "sensitivity_dummy" + sensitivity_path.mkdir() + # Check if error is raised + msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral( + mag_mesh, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + engine="choclo", + ) + + def test_sensitivities_on_disk(self, mag_mesh, receiver_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being stored in disk when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + sensitivities_path = tmp_path / "sensitivities" + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="disk", + sensitivity_path=str(sensitivities_path), + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix was stored in disk and is a memmap + assert sensitivities_path.is_file() + assert type(simulation.G) is np.memmap + + def test_sensitivities_on_ram(self, mag_mesh, receiver_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being allocated in memory when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="ram", + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix is a Numpy array (stored in memory) + assert type(simulation.G) is np.ndarray + + def test_choclo_missing(self, mag_mesh, monkeypatch): + """ + Check if error is raised when choclo is missing and chosen as engine. + """ + # Monkeypatch choclo in simpeg.potential_fields.base + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + # Check if error is raised + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine="choclo") def test_ana_mag_tmi_grad_forward(): + """ + Test TMI gradiometry using susceptibilities as model + """ nx = 61 ny = 61 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) chi1 = 0.01 chi2 = 0.02 @@ -164,11 +711,14 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Problem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) sim = mag.Simulation3DIntegral( @@ -196,9 +746,15 @@ def get_block_inds(grid, block): + prism_2.magnetic_field_gradient(locXyz) + prism_3.magnetic_field_gradient(locXyz) ) * mu_0 - tmi_x = (d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2]) / H0[0] - tmi_y = (d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2]) / H0[0] - tmi_z = (d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2]) / H0[0] + tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude np.testing.assert_allclose(d_x, tmi_x, rtol=1e-10, atol=1e-12) np.testing.assert_allclose(d_y, tmi_y, rtol=1e-10, atol=1e-12) np.testing.assert_allclose(d_z, tmi_z, rtol=1e-10, atol=1e-12) @@ -219,283 +775,40 @@ def get_block_inds(grid, block): ) -def test_ana_mag_grad_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bxx", "bxy", "bxz", "byy", "byz", "bzz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - [rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, - ) - - data = sim.dpred(model_reduced) - d_xx = data[0::6] - d_xy = data[1::6] - d_xz = data[2::6] - d_yy = data[3::6] - d_yz = data[4::6] - d_zz = data[5::6] - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(locXyz) - + prism_2.magnetic_field_gradient(locXyz) - + prism_3.magnetic_field_gradient(locXyz) - ) * mu_0 - - np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=1e-10, atol=1e-12) - - -def test_ana_mag_vec_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) +class TestInvalidMeshChoclo: + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = discretize.TensorMesh(h, "CC") + else: + mesh = discretize.TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + # Build survey + receivers_locations = np.array([[0, 0, 0]]) + receivers = mag.Point(receivers_locations) + sources = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=50_000, + inclination=45.0, + declination=12.0, ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - n_processes=None, - ) - - data = sim.dpred(model_reduced).reshape(-1, 4) - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) - tmi = sim.tmi_projection - - np.testing.assert_allclose(data[:, 0], d[:, 0]) - np.testing.assert_allclose(data[:, 1], d[:, 1]) - np.testing.assert_allclose(data[:, 2], d[:, 2]) - np.testing.assert_allclose(data[:, 3], d @ tmi) - - -def test_ana_mag_amp_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) + survey = mag.Survey(sources) + # Check if error is raised + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - is_amplitude_data=True, - n_processes=None, - ) - - data = sim.dpred(model_reduced) - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) - d_amp = np.linalg.norm(d, axis=1) - - np.testing.assert_allclose(data, d_amp) + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral(mesh, survey, engine="choclo") def test_removed_modeltype(): @@ -513,7 +826,3 @@ def test_removed_modeltype(): message = "modelType has been removed, please use model_type." with pytest.raises(NotImplementedError, match=message): sim.modelType - - -if __name__ == "__main__": - unittest.main() diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index d5b90274fd..67889ba62a 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -169,8 +169,10 @@ # susceptibility model using the integral formulation. # +############################################################################### # Define the forward simulation. By setting the 'store_sensitivities' keyword # argument to "forward_only", we simulate the data without storing the sensitivities + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, @@ -178,9 +180,21 @@ chiMap=model_map, ind_active=ind_active, store_sensitivities="forward_only", + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + +############################################################################### # Compute predicted data for a susceptibility model + dpred = simulation.dpred(model) # Plot diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 0007708f83..70c9621221 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -225,8 +225,10 @@ # in the case of remanent magnetization. # +############################################################################### # Define the forward simulation. By setting the 'store_sensitivities' keyword # argument to "forward_only", we simulate the data without storing the sensitivities + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, @@ -236,7 +238,10 @@ store_sensitivities="forward_only", ) + +############################################################################### # Compute predicted data for some model + dpred = simulation.dpred(model) n_data = len(dpred) diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 24430e337a..92ab0691e0 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -229,15 +229,27 @@ # class. # +############################################################################### # Define the problem. Define the cells below topography and the mapping + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, model_type="scalar", chiMap=model_map, ind_active=active_cells, + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + ####################################################################### # Define Inverse Problem From cbd200b6774b201f2e6f60cda8b35b4118d512cd Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 31 May 2024 13:03:24 -0700 Subject: [PATCH 425/455] Fix typos in EM docstrings (#1473) Fix minor typos in EM docstrings. --- simpeg/electromagnetics/static/resistivity/receivers.py | 2 +- simpeg/electromagnetics/time_domain/simulation.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/simpeg/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py index 3e54d85d96..53c2614bb7 100644 --- a/simpeg/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -210,7 +210,7 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): v : numpy.ndarray The vector which being multiplied adjoint : bool, default = ``False`` - If ``True``, return the ajoint + If ``True``, return the adjoint Returns ------- diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index 8c5f23d006..31720a94c5 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -537,9 +537,10 @@ def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): Returns ------- numpy.ndarray - Derivatives of the intial fields with respect to the model for a given source. - (n_edges or n_faces,) numpy.ndarray when `adjoint` is ``False``. (n_param,) numpy.ndarray - when `ajoint` is ``True``. + Derivatives of the initial fields with respect to the model for + a given source. + (n_edges or n_faces,) numpy.ndarray when ``adjoint`` is ``False``. + (n_param,) numpy.ndarray when ``adjoint`` is ``True``. """ ifieldsDeriv = mkvc( getattr(src, "{}InitialDeriv".format(self._fieldType), None)( From dd7cc94d39c0afd46c9eb108c33ba9a67782496d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 11:36:45 -0700 Subject: [PATCH 426/455] Fix call to private method in GaussianMixtureWithPrior (#1476) Update call of `_print_verbose_msg_init_end` in `GaussianMixtureWithPrior`: since `scikit-learn>=1.5.0` it requires two positional arguments. Use a `try-except` block to maintain support for older versions of `scikit-learn`. Create a new private method for the `GaussianMixtureWithPrior` class to wrap the `_print_verbose_msg_init_end` method including the `try-except` block. Add tests. Fix #1475 --- simpeg/utils/pgi_utils.py | 18 +++++++++- tests/utils/test_gmm_utils.py | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 7141fccc98..638b94cfb3 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -1170,7 +1170,7 @@ def fit_predict(self, X, y=None, debug=False): self.converged_ = True break - self._print_verbose_msg_init_end(lower_bound) + self._custom_print_verbose_msg_init_end(lower_bound) if lower_bound > max_lower_bound or max_lower_bound == -np.inf: max_lower_bound = lower_bound @@ -1193,6 +1193,22 @@ def fit_predict(self, X, y=None, debug=False): return self + def _custom_print_verbose_msg_init_end(self, ll): + """ + Wrapper for the upstream _print_verbose_msg_init_end + + This method was created to provide support of older versions + (scikit-learn<1.5.0) of this private method. + """ + try: + self._print_verbose_msg_init_end(ll, init_has_converged=True) + except TypeError as exception: + # In scikit-learn<1.5.0, the method has a single argument + match = "got an unexpected keyword argument 'init_has_converged'" + if match not in str(exception): + raise + self._print_verbose_msg_init_end(ll) + class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture): """Gaussian mixture class for non-linear relationships. diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py index dbef02a7f5..f6e6c93d18 100644 --- a/tests/utils/test_gmm_utils.py +++ b/tests/utils/test_gmm_utils.py @@ -1,3 +1,4 @@ +import pytest import numpy as np import unittest import discretize @@ -277,5 +278,71 @@ def test_MAP_estimate_multi_component_multidimensions(self): ) +class MockGMMLatest(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with two positional arguments (scikit-learn==1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll, init_has_converged): + """Override upstream method just for test purposes.""" + return None + + +class MockGMMOlder(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with a single positional argument (scikit-learn<1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll): + """Override upstream method just for test purposes.""" + return None + + +class TestCustomPrintMethod: + """ + Test the ``GaussianMixtureWithPrior._print_verbose_msg_init_end`` method + with different signatures of the upstream ``_print_verbose_msg_init_end`` + private method. + """ + + @pytest.fixture + def mesh(self): + """Sample mesh""" + mesh = discretize.TensorMesh([8, 7, 6]) + return mesh + + @pytest.fixture + def model(self, mesh): + """Sample model.""" + model = np.ones(mesh.n_cells, dtype=np.float64) + return model + + @pytest.fixture + def gmmref(self, mesh, model): + """Sample GMM""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + gmmref = WeightedGaussianMixture( + mesh=mesh, + actv=active_cells, + n_components=1, + covariance_type="full", + max_iter=1000, + n_init=10, + tol=1e-8, + warm_start=True, + ) + gmmref.fit(model.reshape(-1, 1)) + return gmmref + + @pytest.mark.parametrize("gmm_class", (MockGMMLatest, MockGMMOlder)) + def test_custom_print_verbose_method(self, gmmref, gmm_class): + """Test custom method against older and latest signature of the upstream one.""" + gmm = gmm_class(gmmref=gmmref) + # Run the custom private method: it should not raise any error + gmm._custom_print_verbose_msg_init_end(3) + + if __name__ == "__main__": unittest.main() From ae63c363f4cc8118c6fcd0e36f8e43419e9f3739 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 16:31:19 -0700 Subject: [PATCH 427/455] Add version switcher to Sphinx docs (#1428) Add a version switcher to the navbar of the docs, using the built in one in PyData Sphinx theme. Point to the experimental `versions.json` file located in `doctest.simpeg.xyz` that should serve the files in [simpeg/simpeg-doctest](https://github.com/simpeg/simpeg-doctest). Add experimental staged to Azure Pipelines that pushes docs to the `simpeg-doctest` repository. One is triggered nightly and deploys the latest docs in `main` to the `dev` branch in `simpeg-doctest` and updates the `dev` submodule in `gh-pages`. The other one is triggered after a release and pushes the new version of the docs to `gh-pages` branch in `simpeg-doctest` onto a new folder, while also updating the `latest` link. --- .github/ISSUE_TEMPLATE/release-checklist.md | 21 +- azure-pipelines.yml | 245 +++++++++++++++++++- docs/_static/versions.json | 31 +++ docs/conf.py | 15 ++ 4 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 docs/_static/versions.json diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index d173424945..543bdb1fdd 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -46,10 +46,9 @@ assignees: "" - [ ] Edit the release notes file, following the template below and the previous release notes. - [ ] Add the new release notes to the list in `docs/content/release/index.rst`. -- [ ] Open a PR with the new release notes. +- [ ] **Open a PR** with the new release notes. - [ ] Manually view the built documentation by downloading the Azure `html_doc` artifact and check for formatting and errors. -- [ ] Merge that PR
@@ -111,6 +110,24 @@ Pull Requests
+### Add new version to version switcher + +Edit the `docs/_static/versions.json` file and: + +- [ ] Add an entry for the new version. +- [ ] Move the line with `"name":` to the new entry (so the new version is set + as the _latest_ one). +- [ ] Update the version number in the `"name":` line. +- [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to + check if the syntax of the JSON file is correct. If no errors are prompted, + then your file is OK. +- [ ] Double-check the changes. +- [ ] Commit the changes to the same branch. + +### Merge the PR + +- [ ] **Merge that PR.** + ## Make the new release - [ ] Draft a new GitHub Release diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f7f7ed21a9..fdad0a95f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,12 +8,22 @@ trigger: include: - '*' +schedules: +- cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + displayName: "Scheduled nightly job" + branches: + include: [ "main" ] + always: false # don't run if no changes have been applied since last sucessful run + batch: false # dont' run if last pipeline is still in-progress + pr: branches: include: - '*' exclude: - '*no-ci*' + + stages: - stage: StyleChecks @@ -75,6 +85,15 @@ stages: python.version: '3.8' timeoutInMinutes: 240 steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + - script: | git config --global user.name ${GH_NAME} git config --global user.email ${GH_EMAIL} @@ -106,6 +125,12 @@ stages: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test @@ -141,4 +166,222 @@ stages: git push displayName: Push documentation to simpeg-docs env: - GH_TOKEN: $(gh.token) \ No newline at end of file + GH_TOKEN: $(gh.token) + +- stage: Deploy_dev_docs_experimental + dependsOn: Testing + condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers + jobs: + - job: + displayName: Deploy dev docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: 'Configure git' + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" + bash Mambaforge.sh -b -p "${HOME}/conda" + displayName: Install mamba + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + source "${HOME}/conda/etc/profile.d/mamba.sh" + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="$(python.version) >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml + conda activate simpeg-test + pip install pytest-azurepipelines + displayName: Create Anaconda testing environment + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + pip install -e . + displayName: Build package + + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest + # and update submodule in the gh-pages branch + - bash: | + # Push new docs + # ------------- + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation (dev branch) + git clone -b dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Remove all files + shopt -s dotglob # configure bash to include dotfiles in * globs + export GLOBIGNORE=".git" # ignore .git directory in glob + git rm -rf * # remove all files + # Copy the built docs to the root of the repo + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . + # Commit the new docs. Amend to avoid having a very large history. + git add . + message="Azure CI deploy dev from ${commit}" + echo -e "\nAmending last commit:" + git commit --amend --reset-author -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (dev branch)." + git push -fq origin dev 2>&1 >/dev/null + echo -e "\nFinished uploading doc files." + + # Update submodule + # ---------------- + # Switch to the gh-pages branch + git switch gh-pages + # Update the dev submodule + git submodule update --init --recursive --remote dev + # Commit changes + git add dev + message="Azure CI update dev submodule from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished updating submodule dev." + + # Unset dotglob + shopt -u dotglob + export GLOBIGNORE="" + displayName: Push documentation to simpeg-doctest (dev branch) + env: + GH_TOKEN: $(gh.token) + +- stage: Deploy_release_docs_experimental + dependsOn: Testing + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + jobs: + - job: + displayName: Deploy release docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: 'Configure git' + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" + bash Mambaforge.sh -b -p "${HOME}/conda" + displayName: Install mamba + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + source "${HOME}/conda/etc/profile.d/mamba.sh" + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="$(python.version) >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml + conda activate simpeg-test + pip install pytest-azurepipelines + displayName: Create Anaconda testing environment + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + pip install -e . + displayName: Build package + + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest + - bash: | + # Capture version + # TODO: we should be able to get the version from the + # build.sourceBranch variable + version=$(git tag --points-at HEAD) + if [ -n "$version" ]; then + echo "Version could not be obtained from tag. Exiting." + exit 1 + fi + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation + git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Move the built docs to a new dev folder + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" + cp $BUILD_SOURCESDIRECTORY/docs/README.md . + # Add .nojekyll if missing + touch .nojekyll + # Update latest symlink + rm -f latest + ln -s "$version" latest + # Commit the new docs. + git add "$version" README.md .nojekyll latest + message="Azure CI deploy ${version} from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished uploading generated files." + displayName: Push documentation to simpeg-doctest + env: + GH_TOKEN: $(gh.token) diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 0000000000..265c9718fa --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,31 @@ +[ + { + "version": "dev", + "url": "https://doctest.simpeg.xyz/dev/" + }, + { + "name": "v0.21.1 (latest)", + "version": "v0.21.1", + "url": "https://doctest.simpeg.xyz/v0.21.1/" + }, + { + "version": "v0.21.0", + "url": "https://doctest.simpeg.xyz/v0.21.0/" + }, + { + "version": "v0.20.0", + "url": "https://doctest.simpeg.xyz/v0.20.0/" + }, + { + "version": "v0.19.0", + "url": "https://doctest.simpeg.xyz/v0.19.0/" + }, + { + "version": "v0.18.1", + "url": "https://doctest.simpeg.xyz/v0.18.1/" + }, + { + "version": "v0.18.0", + "url": "https://doctest.simpeg.xyz/v0.18.0/" + } +] diff --git a/docs/conf.py b/docs/conf.py index 30d36e1446..b71abf3649 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ from sphinx_gallery.sorting import FileNameSortKey import glob import simpeg +from packaging.version import parse import plotly.io as pio from importlib.metadata import version @@ -237,6 +238,13 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] +# Define SimPEG version for the version switcher +simpeg_version = parse(simpeg.__version__) +if simpeg_version.is_devrelease: + switcher_version = "dev" +else: + switcher_version = f"v{simpeg_version.public}" + # Use Pydata Sphinx theme html_theme = "pydata_sphinx_theme" @@ -274,7 +282,14 @@ def linkcode_resolve(domain, info): "plausible_analytics_url": "https://plausible.io/js/script.js", }, "navbar_align": "left", # make elements closer to logo on the left + "navbar_end": ["version-switcher", "theme-switcher", "navbar-icon-links"], + # Configure version switcher + "switcher": { + "version_match": switcher_version, + "json_url": "https://doctest.simpeg.xyz/latest/_static/versions.json", + }, } + html_logo = "images/simpeg-logo.png" html_static_path = ["_static"] From 23d8cd3de6de0b798892beec48ecc57a138fc5ca Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 18:21:28 -0700 Subject: [PATCH 428/455] Use random seed on synthetic data in mag tests (#1457) Make use of the `random_seed` argument of the `make_synthetic_data` method in magnetic tests. Remove the lines that set a global `np.random.seed` in those tests. Part of the solution to #1289 --- tests/pf/test_mag_MVI_Octree.py | 7 +++++-- tests/pf/test_mag_inversion_linear.py | 2 -- tests/pf/test_mag_inversion_linear_Octree.py | 8 +++++--- tests/pf/test_mag_vector_amplitude.py | 7 +++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 5fc3fc68c2..48aa8e26bb 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -19,7 +19,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -106,7 +105,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) # This Mapping connects the regularizations for the three-component diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 2e766c7478..47d8df2321 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -20,8 +20,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # Define the inducing field parameter h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index a78e714e2e..d754d64e5c 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -18,8 +18,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # First we need to define the direction of the inducing field # As a simple case, we pick a vertical inducing field of magnitude # 50,000nT. @@ -111,7 +109,11 @@ def setUp(self): ) self.sim = sim data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=0, ) # Create a regularization diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 65cdde4ddc..5115e4a22a 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -19,7 +19,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -106,7 +105,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) reg = regularization.VectorAmplitude( From f6b8035c2f8324368160facf7b9b323a3b570402 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 11:23:38 -0700 Subject: [PATCH 429/455] Fix links to source code in documentation pages (#1444) Update the `linkcode_resolve` function in Sphinx configuration file so the links to the source code of each function, class or method points to the actual code corresponding to that version, and not always to the code in `main`. Update some Azure configurations: checkout simpeg repo without shallow depth and fetching tags. Clean up the working directory before installing SimPEG so the version doesn't have an extra hash. --------- Co-authored-by: Joseph Capriotti --- .azure-pipelines/matrix.yml | 23 ++++++++++++++++++-- azure-pipelines.yml | 8 ++++++- docs/conf.py | 43 +++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index cc3fa7c170..4269e7bebe 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -23,6 +23,15 @@ jobs: vmImage: ${{ os }} timeoutInMinutes: 120 steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + - script: | wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" bash Mambaforge.sh -b -p "${HOME}/conda" @@ -31,10 +40,14 @@ jobs: - script: | source "${HOME}/conda/etc/profile.d/conda.sh" source "${HOME}/conda/etc/profile.d/mamba.sh" - echo " - python="${{ py_vers }} >> environment_test.yml - mamba env create -f environment_test.yml + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="${{ py_vers }} >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -43,6 +56,12 @@ jobs: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fdad0a95f4..db36785acc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -117,6 +117,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -212,6 +214,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - bash: | @@ -328,6 +332,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - bash: | @@ -384,4 +390,4 @@ stages: echo -e "\nFinished uploading generated files." displayName: Push documentation to simpeg-doctest env: - GH_TOKEN: $(gh.token) + GH_TOKEN: $(gh.token) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index b71abf3649..38bdeef6b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,7 +48,6 @@ "sphinx.ext.mathjax", "sphinx_gallery.gen_gallery", "sphinx.ext.todo", - "sphinx.ext.linkcode", "matplotlib.sphinxext.plot_directive", ] @@ -136,17 +135,24 @@ # edit_on_github_branch = "main/docs" # check_meta = False -# source code links + +# ----------------- +# Source code links +# ----------------- +# Function inspired in matplotlib's configuration + link_github = True -# You can build old with link_github = False if link_github: import inspect - from os.path import relpath, dirname + from packaging.version import parse extensions.append("sphinx.ext.linkcode") def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ if domain != "py": return None @@ -161,44 +167,45 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except Exception: + except AttributeError: return None - try: - unwrap = inspect.unwrap - except AttributeError: - pass - else: - obj = unwrap(obj) - + if inspect.isfunction(obj): + obj = inspect.unwrap(obj) try: fn = inspect.getsourcefile(obj) - except Exception: + except TypeError: fn = None + if not fn or fn.endswith("__init__.py"): + try: + fn = inspect.getsourcefile(sys.modules[obj.__module__]) + except (TypeError, AttributeError, KeyError): + fn = None if not fn: return None try: source, lineno = inspect.getsourcelines(obj) - except Exception: + except (OSError, TypeError): lineno = None if lineno: - linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) + linespec = f"#L{lineno:d}-L{lineno + len(source) - 1:d}" else: linespec = "" try: - fn = relpath(fn, start=dirname(simpeg.__file__)) + fn = os.path.relpath(fn, start=os.path.dirname(simpeg.__file__)) except ValueError: return None - return f"https://github.com/simpeg/simpeg/blob/main/simpeg/{fn}{linespec}" + simpeg_version = parse(simpeg.__version__) + tag = "main" if simpeg_version.is_devrelease else f"v{simpeg_version.public}" + return f"https://github.com/simpeg/simpeg/blob/{tag}/simpeg/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") - # Make numpydoc to generate plots for example sections numpydoc_use_plots = True plot_pre_code = """ From 30fadd308a796e292c220b94e968c9502ab26e3f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 12:59:45 -0700 Subject: [PATCH 430/455] Fix script for new deployment of docs (#1478) Fix issues in the new (experimental) scripts for deployment of docs to `simpeg-doctest`. Fetch the `gh-pages` before trying to checkout it. This is needed because we clone the repo with `--depth 1`. When passing this option, git appends the `--single-branch` option meaning the `dev` branch is the only branch that is being cloned. Improve how we clone the `simpeg-doctest` repo: clone quietly to reduce output lines, and specify the branch while deploying release docs (in case the default branch in the repo gets changed). --- azure-pipelines.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index db36785acc..7866c36069 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -245,7 +245,7 @@ stages: # Capture hash of last commit in simpeg commit=$(git rev-parse --short HEAD) # Clone the repo where we store the documentation (dev branch) - git clone -b dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + git clone -q --branch dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git cd simpeg-doctest # Remove all files shopt -s dotglob # configure bash to include dotfiles in * globs @@ -266,6 +266,9 @@ stages: # Update submodule # ---------------- + # Need to fetch the gh-pages branch first (because we clone with + # shallow depth) + git fetch --depth 1 origin gh-pages:gh-pages # Switch to the gh-pages branch git switch gh-pages # Update the dev submodule @@ -278,7 +281,7 @@ stages: # Make the push quiet just in case there is anything that could # leak sensitive information. echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." - git push -fq origin gh-pages 2>&1 >/dev/null + git push -q origin gh-pages 2>&1 >/dev/null echo -e "\nFinished updating submodule dev." # Unset dotglob @@ -368,7 +371,7 @@ stages: # Capture hash of last commit in simpeg commit=$(git rev-parse --short HEAD) # Clone the repo where we store the documentation - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git cd simpeg-doctest # Move the built docs to a new dev folder cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" From 772429a0a2e8aa5a59d14d9e42262dcf667bae9a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 15:13:30 -0700 Subject: [PATCH 431/455] Print SimPEG version in the inversion log (#1477) Include the version of SimPEG being used to run a simulation in the inversion log. Fixes #1474 --- simpeg/inverse_problem.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index b4f5630bd9..e554c95cee 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -14,13 +14,22 @@ validate_ndarray_with_shape, ) from .simulation import DefaultSolver +from .version import __version__ as simpeg_version class BaseInvProblem: """BaseInvProblem(dmisfit, reg, opt)""" def __init__( - self, dmisfit, reg, opt, beta=1.0, debug=False, counter=None, **kwargs + self, + dmisfit, + reg, + opt, + beta=1.0, + debug=False, + counter=None, + print_version=True, + **kwargs, ): super().__init__(**kwargs) assert isinstance(reg, BaseRegularization) or isinstance( @@ -35,6 +44,7 @@ def __init__( self.debug = debug self.counter = counter self.model = None + self.print_version = print_version # TODO: Remove: (and make iteration printers better!) self.opt.parent = self self.reg.parent = self @@ -174,6 +184,9 @@ def startup(self, m0): if self.debug: print("Calling InvProblem.startup") + if self.print_version: + print(f"\nRunning inversion with SimPEG v{simpeg_version}") + for fct in self.reg.objfcts: if ( hasattr(fct, "reference_model") From caa60b71a25d87319111ffcd948b03cf6a01a43f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 7 Jun 2024 12:58:57 -0700 Subject: [PATCH 432/455] Use Numpy rng in FDEM tests (#1449) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the FDEM tests. Increase number of iterations for checking derivatives where needed. Part of the solution to #1289 --------- Co-authored-by: Lindsey Heagy --- tests/em/fdem/forward/test_FDEM_casing.py | 22 ++++++++++++++----- tests/em/fdem/forward/test_FDEM_primsec.py | 8 +++---- tests/em/fdem/forward/test_properties.py | 9 ++++---- .../inverse/adjoint/test_FDEM_adjointEB.py | 9 ++++---- .../inverse/adjoint/test_FDEM_adjointHJ.py | 9 ++++---- .../fdem/inverse/derivs/test_FDEM_derivs.py | 7 +++--- tests/em/fdem/muinverse/test_muinverse.py | 21 ++++++++++-------- 7 files changed, 50 insertions(+), 35 deletions(-) diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index f1aa75a736..a0ceffc3c8 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -12,9 +12,10 @@ sigma = np.r_[10.0, 5.5e6, 1e-1] mu = mu_0 * np.r_[1.0, 100.0, 1.0] srcloc = np.r_[0.0, 0.0, 0.0] -xobs = np.random.rand(n) + 10.0 +rng = np.random.default_rng(seed=42) +xobs = rng.uniform(size=n) + 10.0 yobs = np.zeros(n) -zobs = np.random.randn(n) +zobs = rng.normal(size=n) def CasingMagDipoleDeriv_r(x): @@ -63,15 +64,24 @@ def CasingMagDipole2Deriv_z_z(z): class Casing_DerivTest(unittest.TestCase): def test_derivs(self): + rng = np.random.default_rng(seed=42) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipoleDeriv_r, np.ones(n) * 10 + np.random.randn(n), plotIt=False + CasingMagDipoleDeriv_r, np.ones(n) * 10 + rng.normal(size=n), plotIt=False ) - tests.check_derivative(CasingMagDipoleDeriv_z, np.random.randn(n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative + tests.check_derivative(CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( CasingMagDipole2Deriv_z_r, - np.ones(n) * 10 + np.random.randn(n), + np.ones(n) * 10 + rng.normal(size=n), plotIt=False, ) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipole2Deriv_z_z, np.random.randn(n), plotIt=False + CasingMagDipole2Deriv_z_z, rng.normal(size=n), plotIt=False ) diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index c66b688bd0..a4e4494871 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -16,8 +16,6 @@ TOL_JT = 1e-10 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order -np.random.seed(2016) - # To test the primary secondary-source, we look at make sure doing primary # secondary for a simple model gives comprable results to just solving a 3D # problem @@ -128,6 +126,7 @@ def fun(x): lambda x: self.secondarySimulation.Jvec(x0, x, f=self.fields_primsec), ] + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False) def AdjointTest(self): @@ -135,8 +134,9 @@ def AdjointTest(self): m = model f = self.fields_primsec - v = np.random.rand(self.secondarySurvey.nD) - w = np.random.rand(self.secondarySimulation.sigmaMap.nP) + rng = np.random.default_rng(seed=2016) + v = rng.uniform(size=self.secondarySurvey.nD) + w = rng.uniform(size=self.secondarySimulation.sigmaMap.nP) vJw = v.dot(self.secondarySimulation.Jvec(m, w, f)) wJtv = w.dot(self.secondarySimulation.Jtvec(m, v, f)) diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index 2b20b8c900..c583907f7e 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -44,12 +44,13 @@ def test_source_properties_validation(): # LineCurrent with pytest.raises(TypeError): fdem.sources.LineCurrent([], frequency, location=["a", "b", "c"]) + rng = np.random.default_rng(seed=42) + random_locations = rng.normal(size=(5, 3, 2)) with pytest.raises(ValueError): - fdem.sources.LineCurrent([], frequency, location=np.random.rand(5, 3, 2)) + fdem.sources.LineCurrent([], frequency, location=random_locations) + random_locations = rng.normal(size=(5, 3)) with pytest.raises(ValueError): - fdem.sources.LineCurrent( - [], frequency, location=np.random.rand(5, 3), current=0.0 - ) + fdem.sources.LineCurrent([], frequency, location=random_locations, current=0.0) def test_bad_source_type(): diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py index 618d133b5b..260227a860 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py @@ -26,17 +26,18 @@ def adjointTest(fdemType, comp): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey # prb.PropMap.PropModel.mu = mu # prb.PropMap.PropModel.mui = 1./mu u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py index 4713ff4611..bb0dcac83b 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py @@ -26,15 +26,16 @@ def adjointTest(fdemType, comp, src): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py index 7434ade3e1..c30005ba36 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -29,19 +29,18 @@ def derivTest(fdemType, comp, src): prb = getFDEMProblem(fdemType, comp, SrcType, freq) - # prb.solverOpts = dict(check_accuracy=True) print(f"{fdemType} formulation {src} - {comp}") x0 = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) - # mu = np.log(np.ones(prb.mesh.nC)*MU) if addrandoms is True: - x0 = x0 + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - # mu = mu + np.random.randn(prb.sigmaMap.nP)*MU*1e-1 + rng = np.random.default_rng(seed=42) + x0 = x0 + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 def fun(x): return prb.dpred(x), lambda x: prb.Jvec(x0, x) + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 50864fa9e3..7e3841d396 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -18,8 +18,9 @@ def setupMeshModel(): hz = [(cs, npad, -1.3), (cs, nc), (cs, npad, 1.3)] mesh = discretize.CylindricalMesh([hx, 1.0, hz], "0CC") - muMod = 1 + MuMax * np.random.randn(mesh.nC) - sigmaMod = np.random.randn(mesh.nC) + rng = np.random.default_rng(seed=2016) + muMod = 1 + MuMax * rng.normal(size=mesh.nC) + sigmaMod = rng.normal(size=mesh.nC) return mesh, muMod, sigmaMod @@ -149,7 +150,8 @@ def test_mats_cleared(self): MfMuiIDeriv_zero = self.simulation.MfMuiIDeriv(utils.Zero()) MeMuDeriv_zero = self.simulation.MeMuDeriv(utils.Zero()) - m1 = np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=2016) + m1 = rng.uniform(size=self.mesh.nC) self.simulation.model = m1 self.assertTrue(getattr(self, "_MeMu", None) is None) @@ -168,7 +170,6 @@ def JvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(3321) mod = self.m0 def fun(x): @@ -177,9 +178,11 @@ def fun(x): lambda x: self.simulation.Jvec(mod, x), ) - dx = np.random.rand(*mod.shape) * (mod.max() - mod.min()) * 0.01 + rng = np.random.default_rng(seed=3321) + dx = rng.uniform(size=mod.shape) * (mod.max() - mod.min()) * 0.01 - return tests.check_derivative(fun, mod, dx=dx, num=3, plotIt=False) + np.random.seed(1983) # set a random seed for check_derivative + return tests.check_derivative(fun, mod, dx=dx, num=4, plotIt=False) def JtvecTest( self, prbtype="ElectricField", sigmaInInversion=False, invertMui=False @@ -187,9 +190,9 @@ def JtvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(31345) - u = np.random.rand(self.simulation.muMap.nP) - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=3321) + u = rng.uniform(size=self.simulation.muMap.nP) + v = rng.uniform(size=self.survey.nD) self.simulation.model = self.m0 From efad8b0e174caac997c4eafd8f489e42a5bb8e5f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 7 Jun 2024 15:26:55 -0700 Subject: [PATCH 433/455] Add `random_seed` argument to objective fun's derivative tests (#1448) Add a new `random_seed` argument to the `test()` method of objective functions to control their random state. Use Numpy's random number generator for managing the random state and the generation of random numbers. Minor improvements to the implementation of the test methods. Update tests that make use of these methods, and make them to use a seed in every case. Part of the solution to #1289 --- simpeg/objective_function.py | 49 +++++++++++++------ .../regularizations/test_cross_gradient.py | 32 +++++------- tests/base/regularizations/test_jtv.py | 8 +-- tests/base/test_correspondance.py | 4 +- tests/base/test_joint.py | 4 +- tests/base/test_objective_function.py | 13 ++--- 6 files changed, 61 insertions(+), 49 deletions(-) diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index 1e864d1b6e..e28bec0c90 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -9,6 +9,7 @@ from .maps import IdentityMap from .props import BaseSimPEG from .utils import timeIt, Zero, Identity +from .typing import RandomSeed __all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] @@ -194,27 +195,38 @@ def deriv2(self, m, v=None, **kwargs): ) ) - def _test_deriv(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv".format(self.__class__.__name__)) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) - + rng = np.random.default_rng(seed=random_seed) + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) return check_derivative( lambda m: [self(m), self.deriv(m)], x, num=num, plotIt=plotIt, **kwargs ) - def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv2( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv2".format(self.__class__.__name__)) + rng = np.random.default_rng(seed=random_seed) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) - v = x + 0.1 * np.random.rand(len(x)) + v = x + 0.1 * rng.uniform(size=len(x)) expectedOrder = kwargs.pop("expectedOrder", 1) return check_derivative( lambda m: [self.deriv(m).dot(v), self.deriv2(m, v=v)], @@ -225,7 +237,7 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): **kwargs, ) - def test(self, x=None, num=4, **kwargs): + def test(self, x=None, num=4, random_seed: RandomSeed | None = None, **kwargs): """Run a convergence test on both the first and second derivatives. They should be second order! @@ -236,6 +248,11 @@ def test(self, x=None, num=4, **kwargs): The evaluation point for the Taylor expansion. num : int The number of iterations in the convergence test. + random_seed : :class:`~simpeg.typing.RandomSeed` or None, optional + Random seed used for generating a random array for ``x`` if it's + None, and the ``v`` array for testing the second derivatives. It + can either be an int, a predefined Numpy random number generator, + or any valid input to ``numpy.random.default_rng``. Returns ------- @@ -243,8 +260,10 @@ def test(self, x=None, num=4, **kwargs): ``True`` if both tests pass. ``False`` if either test fails. """ - deriv = self._test_deriv(x=x, num=num, **kwargs) - deriv2 = self._test_deriv2(x=x, num=num, plotIt=False, **kwargs) + deriv = self._test_deriv(x=x, num=num, random_seed=random_seed, **kwargs) + deriv2 = self._test_deriv2( + x=x, num=num, plotIt=False, random_seed=random_seed, **kwargs + ) return deriv & deriv2 __numpy_ufunc__ = True diff --git a/tests/base/regularizations/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py index 4b5741a7cb..65f21ea4b7 100644 --- a/tests/base/regularizations/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -40,10 +40,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -51,11 +50,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -135,10 +133,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -146,11 +143,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -214,10 +210,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -225,11 +220,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -282,10 +276,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -293,11 +286,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) diff --git a/tests/base/regularizations/test_jtv.py b/tests/base/regularizations/test_jtv.py index 29239f36d7..e30d570c5e 100644 --- a/tests/base/regularizations/test_jtv.py +++ b/tests/base/regularizations/test_jtv.py @@ -46,7 +46,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -96,7 +96,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -143,7 +143,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -192,7 +192,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index e4cd6cac3f..8e92fdb848 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -43,8 +43,8 @@ def test_order_full_hessian(self): """ corr = self.corr - self.assertTrue(corr._test_deriv()) - self.assertTrue(corr._test_deriv2(expectedOrder=2)) + self.assertTrue(corr._test_deriv(random_seed=10)) + self.assertTrue(corr._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): m = np.random.randn(2 * len(self.mesh)) diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index f856239edc..3d269cb141 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -72,8 +72,8 @@ def setUp(self): self.dmiscombo = self.dmis0 + self.dmis1 def test_multiDataMisfit(self): - self.dmis0.test() - self.dmis1.test() + self.dmis0.test(random_seed=42) + self.dmis1.test(random_seed=42) self.dmiscombo.test(x=self.model) def test_inv(self): diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 060dcf907c..ad4a3d22ac 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -57,7 +57,7 @@ def test_scalarmul(self): objfct_c = objfct_a + objfct_b self.assertTrue(scalar * objfct_a(m) == objfct_b(m)) - self.assertTrue(objfct_b.test()) + self.assertTrue(objfct_b.test(random_seed=42)) self.assertTrue(objfct_c(m) == objfct_a(m) + objfct_b(m)) self.assertTrue(len(objfct_c.objfcts) == 2) @@ -126,7 +126,7 @@ def test_3sum(self): self.assertTrue(len(phi.objfcts) == 3) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_sum_fail(self): nP1 = 10 @@ -166,7 +166,7 @@ def test_ZeroObjFct(self): + utils.Zero() * objective_function.L2ObjectiveFunction() ) self.assertTrue(len(phi.objfcts) == 1) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_updateMultipliers(self): nP = 10 @@ -257,9 +257,10 @@ def test_Maps(self): self.assertTrue(objfct3(m) == objfct1(m) + objfct2(m)) - objfct1.test() - objfct2.test() - objfct3.test() + seed = 42 + objfct1.test(random_seed=seed) + objfct2.test(random_seed=seed) + objfct3.test(random_seed=seed) def test_ComboW(self): nP = 15 From 1cb0a55b457358c263896ca1ae063a7ba6f7efe5 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 9 Jun 2024 14:26:00 -0700 Subject: [PATCH 434/455] Add model_map back --- simpeg/data_misfit.py | 15 ++++++++++++++- simpeg/maps.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index 6b489b425a..b6e4dda280 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -36,12 +36,25 @@ class inherits the :py:class:`simpeg.objective_function.L2ObjectiveFunction`. Assign a SimPEG ``Counter`` object to store iterations and run-times. """ - def __init__(self, data, simulation, debug=False, counter=None, **kwargs): + def __init__(self, data, simulation, model_map=None, debug=False, counter=None, **kwargs): super().__init__(has_fields=True, debug=debug, counter=counter, **kwargs) self.data = data self.simulation = simulation + self.model_map = model_map + + @property + def model_map(self): + return getattr(self, "_model_map", None) + + @model_map.setter + def model_map(self, value): + if value is None: + value = Identity() + self._model_map = value + self._has_fields = True + @property def data(self): """A SimPEG data object. diff --git a/simpeg/maps.py b/simpeg/maps.py index 7e1ccc2baf..09ef514cb2 100644 --- a/simpeg/maps.py +++ b/simpeg/maps.py @@ -6147,7 +6147,7 @@ def __init__( self._components = validate_integer("components", components, min_val=1) self.enforce_active = enforce_active # trigger creation of P - self._projection = None + _ = self.projection @property def global_mesh(self): From 9e95650f2f81d48ca160dd82ce7f1bf7f31e4362 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 9 Jun 2024 16:13:08 -0700 Subject: [PATCH 435/455] Add getJtJdiag --- simpeg/dask/potential_fields/base.py | 7 +++++- .../potential_fields/gravity/simulation.py | 1 - .../potential_fields/magnetics/simulation.py | 1 - simpeg/data_misfit.py | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/simpeg/dask/potential_fields/base.py b/simpeg/dask/potential_fields/base.py index 3f6ad2ea0e..acccec103c 100644 --- a/simpeg/dask/potential_fields/base.py +++ b/simpeg/dask/potential_fields/base.py @@ -43,7 +43,7 @@ def dask_residual(self, m, dobs, f=None): def dask_linear_operator(self): - forward_only = self.store_sensitivities is None + forward_only = self.store_sensitivities == "forward_only" row = delayed(self.evaluate_integral, pure=True) n_cells = self.nC if getattr(self, "model_type", None) == "vector": @@ -112,3 +112,8 @@ def dask_linear_operator(self): Sim.linear_operator = dask_linear_operator + +def compute_J(self): + return self.getJ(self.model) + +Sim.compute_J = compute_J diff --git a/simpeg/dask/potential_fields/gravity/simulation.py b/simpeg/dask/potential_fields/gravity/simulation.py index 780e37057a..90f58acccf 100644 --- a/simpeg/dask/potential_fields/gravity/simulation.py +++ b/simpeg/dask/potential_fields/gravity/simulation.py @@ -2,7 +2,6 @@ from ....potential_fields.gravity import Simulation3DIntegral as Sim from ....utils import sdiag, mkvc - def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ diff --git a/simpeg/dask/potential_fields/magnetics/simulation.py b/simpeg/dask/potential_fields/magnetics/simulation.py index 5682066d2f..9241894ce8 100644 --- a/simpeg/dask/potential_fields/magnetics/simulation.py +++ b/simpeg/dask/potential_fields/magnetics/simulation.py @@ -2,7 +2,6 @@ from ....potential_fields.magnetics import Simulation3DIntegral as Sim from ....utils import sdiag, mkvc - def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index b6e4dda280..9db76f7a29 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -346,3 +346,25 @@ def deriv2(self, m, v, f=None): return 2 * self.simulation.Jtvec_approx( m, self.W * (self.W * self.simulation.Jvec_approx(m, v, f=f)), f=f ) + + def getJtJdiag(self, m): + """ + Evaluate the main diagonal of JtJ + """ + if getattr(self.simulation, "getJtJdiag", None) is None: + raise AttributeError( + "Simulation does not have a getJtJdiag attribute." + + "Cannot form the sensitivity explicitly" + ) + + mapping_deriv = self.model_map.deriv(m) + + if self.model_map is not None: + m = mapping_deriv @ m + + jtjdiag = self.simulation.getJtJdiag(m, W=self.W) + + if self.model_map is not None: + jtjdiag = mkvc((sdiag(np.sqrt(jtjdiag)) @ mapping_deriv).power(2).sum(axis=0)) + + return jtjdiag \ No newline at end of file From 943abee84b015d9052271217d9d27445f9a7d716 Mon Sep 17 00:00:00 2001 From: domfournier Date: Sun, 9 Jun 2024 16:33:16 -0700 Subject: [PATCH 436/455] Fix reference to compute_J --- simpeg/dask/potential_fields/base.py | 2 +- simpeg/data_misfit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simpeg/dask/potential_fields/base.py b/simpeg/dask/potential_fields/base.py index acccec103c..58ae806431 100644 --- a/simpeg/dask/potential_fields/base.py +++ b/simpeg/dask/potential_fields/base.py @@ -114,6 +114,6 @@ def dask_linear_operator(self): Sim.linear_operator = dask_linear_operator def compute_J(self): - return self.getJ(self.model) + return self.linear_operator() Sim.compute_J = compute_J diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index 9db76f7a29..fbbd41b444 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -1,5 +1,5 @@ import numpy as np -from .utils import Counter, sdiag, timeIt, Identity, validate_type +from .utils import Counter, mkvc, sdiag, timeIt, Identity, validate_type from .data import Data from .simulation import BaseSimulation from .objective_function import L2ObjectiveFunction From 6e126dbac3248624567cc818b3e7dd0bf2a73875 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 09:17:44 -0700 Subject: [PATCH 437/455] Add back old beta estimator --- simpeg/directives/directives.py | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 764aa6565b..68dead31f0 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -496,6 +496,74 @@ def initialize(self): self.invProb.beta = self.beta0 +class BetaEstimateDerivative(BaseBetaEstimator): + r"""Estimate initial trade-off parameter (beta) using largest derivatives. + + The initial trade-off parameter (beta) is estimated by scaling the ratio + between the largest derivatives in the gradient of the data misfit and + model objective function. The estimated trade-off parameter is used to + update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` + object prior to running the inversion. A separate directive is used for updating the + trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. + + Parameters + ---------- + beta0_ratio: float + Desired ratio between data misfit and model objective function at initial beta iteration. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + + Notes + ----- + Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model + objective function and :math:`\mathbf{m_0}` represent the starting model. The first + model update is obtained by minimizing the a global objective function of the form: + + .. math:: + \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) + + where :math:`\beta_0` represents the initial trade-off parameter (beta). + + We define :math:`\gamma` as the desired ratio between the data misfit and model objective + functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). + Here, the initial trade-off parameter is computed according to: + + .. math:: + \beta_0 = \gamma \frac{| \nabla_m \phi_d (\mathbf{m_0}) |_{max}}{| \nabla_m \phi_m (\mathbf{m_0 + \delta m}) |_{max}} + + where + + .. math:: + \delta \mathbf{m} = \frac{m_{max}}{\mu_{max}} \boldsymbol{\mu} + + and :math:`\boldsymbol{\mu}` is a set of independent samples from the + continuous uniform distribution between 0 and 1. + + """ + + def __init__(self, beta0_ratio=1.0, seed: RandomSeed | None = None, **kwargs): + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) + + def initialize(self): + rng = np.random.default_rng(seed=self.seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + x0 = rng.random(size=m.shape) + phi_d_deriv = self.dmisfit.deriv2(m, x0) + t = np.dot(x0, phi_d_deriv) + reg = self.reg.deriv2(m, v=x0) + b = np.dot(x0, reg) + self.ratio = np.asarray(t / b) + self.beta0 = self.beta0_ratio * self.ratio + self.invProb.beta = self.beta0 + + class BetaEstimate_ByEig(BaseBetaEstimator): r"""Estimate initial trade-off parameter (beta) by power iteration. From fc6dbdd64a3c7c7c7f8c85c4f2014af4578b26ca Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 09:24:26 -0700 Subject: [PATCH 438/455] Update version and init --- pyproject.toml | 2 +- simpeg/directives/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4768216d17..a8193e5a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "Mira-SimPEG" -version = "0.19.0.9-alpha.1" +version = "0.21.2.1-alpha.1" license = "MIT" description = "Mira Geoscience fork of SimPEG: Simulation and Parameter Estimation in Geophysics" diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index 69081767d1..332fbef401 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -100,6 +100,7 @@ from .directives import ( InversionDirective, DirectiveList, + BetaEstimateDerivative, BetaEstimateMaxDerivative, BetaEstimate_ByEig, BetaSchedule, From 92b49a3a960be8ad65903d88be8ed4ada9a90b2e Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 10:35:04 -0700 Subject: [PATCH 439/455] Fix mag kernel for forward only. Add back Vector directive --- simpeg/directives/__init__.py | 1 + simpeg/potential_fields/magnetics/simulation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index 332fbef401..7da1162152 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -117,6 +117,7 @@ ScalingMultipleDataMisfits_ByEig, JointScalingSchedule, UpdateSensitivityWeights, + VectorInversion, ProjectSphericalBounds, ) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 167329185c..f960e63810 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -561,7 +561,7 @@ def evaluate_integral(self, receiver_location, components): + cell_eval_z * M[:, 2] ) - if self.store_sensitivities is None: + if self.store_sensitivities == "forward_only": rows[component] = cell_vals @ self.chi else: rows[component] = cell_vals From f19c42c2d5a893571f8da2db15eee42316db8f40 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 12:36:28 -0700 Subject: [PATCH 440/455] Update simpeg directive SaveGeoh5 --- simpeg/directives/__init__.py | 1 + simpeg/directives/directives.py | 103 +++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index 7da1162152..b0d750d869 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -118,6 +118,7 @@ JointScalingSchedule, UpdateSensitivityWeights, VectorInversion, + SaveIterationsGeoH5, ProjectSphericalBounds, ) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 68dead31f0..d58e04c59d 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,6 +1,7 @@ from __future__ import annotations # needed to use type operands in Python 3.8 from pathlib import Path +from datetime import datetime import numpy as np import matplotlib.pyplot as plt @@ -50,7 +51,8 @@ from geoh5py.workspace import Workspace from geoh5py.objects import ObjectBase -from datetime import datetime +from geoh5py.ui_json.utils import fetch_active_workspace + class InversionDirective: @@ -3029,10 +3031,49 @@ def stack_channels(self, dpred: list): return self.reshape(np.hstack(dpred)) - def save_components(self, iteration: int, values: list[np.ndarray] = None): + def apply_transformations(self, prop: np.ndarray) -> np.ndarray: """ - Sort, transform and store data per components and channels. + Re-order the values and apply transformations. """ + prop = prop.flatten() + for fun in self.transforms: + if isinstance(fun, (IdentityMap, np.ndarray, float)): + prop = fun * prop + else: + prop = fun(prop) + + if prop.ndim == 2: + prop = prop.T.flatten() + + prop = prop.reshape((len(self.channels), len(self.components), -1)) + + return prop + + def get_names( + self, component: str, channel: str, iteration: int + ) -> tuple[str, str]: + """ + Format the data and property_group name. + """ + base_name = f"Iteration_{iteration}" + if len(component) > 0: + base_name += f"_{component}" + + channel_name = base_name + if channel: + channel_name += f"_{channel}" + + if self.label is not None: + channel_name += f"_{self.label}" + base_name += f"_{self.label}" + + return channel_name, base_name + + def get_values(self, values: list[np.ndarray] | None): + """ + Get values for the inversion depending on the output type. + """ + prop = self.invProb.model if values is not None: prop = self.stack_channels(values) elif self.attribute_type == "predicted": @@ -3047,28 +3088,27 @@ def save_components(self, iteration: int, values: list[np.ndarray] = None): prop = self.stack_channels(dpred) elif self.attribute_type == "sensitivities": for directive in self.inversion.directiveList.dList: - if isinstance(directive, UpdateSensitivityWeights): + if isinstance(directive, directives.UpdateSensitivityWeights): prop = self.reshape(np.sum(directive.JtJdiag, axis=0) ** 0.5) - else: - prop = self.invProb.model - # Apply transformations - prop = prop.flatten() - for fun in self.transforms: - if isinstance(fun, (IdentityMap, np.ndarray, float)): - prop = fun * prop - else: - prop = fun(prop) + return prop - if prop.ndim == 2: - prop = prop.T.flatten() + def save_components( # flake8: noqa + self, iteration: int, values: list[np.ndarray] = None + ): + """ + Sort, transform and store data per components and channels. + """ + prop = self.get_values(values) - prop = prop.reshape((len(self.channels), len(self.components), -1)) + # Apply transformations + prop = self.apply_transformations(prop) - with Workspace(self._h5_file) as w_s: + # Save results + with fetch_active_workspace(self._geoh5, mode="r+") as w_s: h5_object = w_s.get_entity(self.h5_object)[0] for cc, component in enumerate(self.components): - if component not in self.data_type.keys(): + if component not in self.data_type: self.data_type[component] = {} for ii, channel in enumerate(self.channels): @@ -3077,17 +3117,9 @@ def save_components(self, iteration: int, values: list[np.ndarray] = None): if self.sorting is not None: values = values[self.sorting] - base_name = f"Iteration_{iteration}" - if len(component) > 0: - base_name += f"_{component}" - - channel_name = base_name - if channel: - channel_name += f"_{channel}" - - if self.label is not None: - channel_name += f"_{self.label}" - base_name += f"_{self.label}" + channel_name, base_name = self.get_names( + component, channel, iteration + ) data = h5_object.add_data( { @@ -3116,14 +3148,14 @@ def write_update(self, iteration: int): """ Write update to file. """ - dirpath = Path(self._h5_file).parent + dirpath = Path(self._geoh5.h5file).parent filepath = dirpath / "SimPEG.out" if iteration == 0: - with open(filepath, "w") as f: + with open(filepath, "w", encoding="utf-8") as f: f.write("iteration beta phi_d phi_m time\n") - with open(filepath, "a") as f: + with open(filepath, "a", encoding="utf-8") as f: date_time = datetime.now().strftime("%b-%d-%Y:%H:%M:%S") f.write( f"{iteration} {self.invProb.beta:.3e} {self.invProb.phi_d:.3e} " @@ -3134,9 +3166,9 @@ def save_log(self): """ Save iteration metrics to comments. """ - dirpath = Path(self._h5_file).parent + dirpath = Path(self._geoh5.h5file).parent - with Workspace(self._h5_file) as w_s: + with fetch_active_workspace(self._geoh5, mode="r+") as w_s: h5_object = w_s.get_entity(self.h5_object)[0] for file in ["SimPEG.out", "SimPEG.log"]: @@ -3227,7 +3259,7 @@ def h5_object(self, entity: ObjectBase): ) self._h5_object = entity.uid - self._h5_file = entity.workspace.h5file + self._geoh5 = entity.workspace if getattr(entity, "n_cells", None) is not None: self.association = "CELL" @@ -3248,6 +3280,7 @@ def association(self, value): self._association = value.upper() + class VectorInversion(InversionDirective): """ Control a vector inversion from Cartesian to spherical coordinates From c399655d710ada5e8ac8bbef857b47232a734082 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 13:20:09 -0700 Subject: [PATCH 441/455] Bring back sparse receiver derivatives --- .../natural_source/receivers.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index d3d7d3de6c..728552a9d8 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -2,7 +2,7 @@ import numpy as np from scipy.constants import mu_0 - +from scipy.sparse import csr_matrix from ...survey import BaseRx @@ -299,10 +299,18 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals n_d = self.nD if mesh.dim == 3: - ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v[..., None] - ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v[..., None] - ge_v = np.c_[h[:, 1], -h[:, 0]] * gtop_v[..., None] - gh_v = np.c_[-e[:, 1], e[:, 0]] * gtop_v[..., None] + ghx_v = np.einsum( + "ij,ik->ijk", gbot_v, np.c_[hy[:, 1], -hy[:, 0]] + ).reshape((hy.shape[0], -1)) + ghy_v = np.einsum( + "ij,ik->ijk", gbot_v, np.c_[-hx[:, 1], hx[:, 0]] + ).reshape((hx.shape[0], -1)) + ge_v = np.einsum( + "ij,ik->ijk", gtop_v, np.c_[h[:, 1], -h[:, 0]] + ).reshape((h.shape[0], -1)) + gh_v = np.einsum( + "ij,ik->ijk", gtop_v, np.c_[-e[:, 1], e[:, 0]] + ).reshape((e.shape[0], -1)) if self.orientation[1] == "x": ghy_v += gh_v @@ -311,9 +319,9 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals if v.ndim == 2: # collapse into a long list of n_d vectors - ghx_v = ghx_v.reshape((n_d, -1)) - ghy_v = ghy_v.reshape((n_d, -1)) - ge_v = ge_v.reshape((n_d, -1)) + ghx_v = csr_matrix(ghx_v.reshape((n_d, -1))) + ghy_v = csr_matrix(ghy_v.reshape((n_d, -1))) + ge_v = csr_matrix(ge_v.reshape((n_d, -1))) gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v ge_v = Pe.T @ ge_v @@ -535,9 +543,8 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): if adjoint: # Work backwards! - gtop_v = (v / bot)[..., None] - gbot_v = (-imp * v / bot)[..., None] - n_d = self.nD + gtop_v = np.c_[v] / bot[:, None] + gbot_v = -imp[:, None] * np.c_[v] / bot[:, None] ghx_v = np.einsum("ij,ik->ijk", gbot_v, np.c_[hy[:, 1], -hy[:, 0]]).reshape( (hy.shape[0], -1) @@ -557,14 +564,11 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): else: ghx_v += gh_v - if v.ndim == 2: - # collapse into a long list of n_d vectors - ghx_v = ghx_v.reshape((n_d, -1)) - ghy_v = ghy_v.reshape((n_d, -1)) - ghz_v = ghz_v.reshape((n_d, -1)) - - gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Phz.T @ ghz_v - + gh_v = ( + Phx.T @ csr_matrix(ghx_v) + + Phy.T @ csr_matrix(ghy_v) + + Phz.T @ csr_matrix(ghz_v) + ) return f._hDeriv(src, None, gh_v, adjoint=True) dh_v = f._hDeriv(src, du_dm_v, v, adjoint=False) From 6ee49f5e6dc285c0af327ce31a910e9c067357f3 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 10 Jun 2024 14:21:46 -0700 Subject: [PATCH 442/455] Add back sensitivity storage to base class --- .../frequency_domain/simulation.py | 1 - .../time_domain/simulation.py | 2 -- simpeg/simulation.py | 24 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/simpeg/dask/electromagnetics/frequency_domain/simulation.py b/simpeg/dask/electromagnetics/frequency_domain/simulation.py index 68891f7f62..8e308b98cd 100644 --- a/simpeg/dask/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/dask/electromagnetics/frequency_domain/simulation.py @@ -14,7 +14,6 @@ Sim.sensitivity_path = "./sensitivity/" Sim.gtgdiag = None -Sim.store_sensitivities = True Sim.getJtJdiag = dask_getJtJdiag Sim.Jvec = dask_Jvec diff --git a/simpeg/dask/electromagnetics/time_domain/simulation.py b/simpeg/dask/electromagnetics/time_domain/simulation.py index 64b3125ac2..03cc103f0f 100644 --- a/simpeg/dask/electromagnetics/time_domain/simulation.py +++ b/simpeg/dask/electromagnetics/time_domain/simulation.py @@ -17,8 +17,6 @@ from tqdm import tqdm Sim.sensitivity_path = "./sensitivity/" -Sim.store_sensitivities = "ram" - Sim.getJtJdiag = dask_getJtJdiag Sim.Jvec = dask_Jvec Sim.Jtvec = dask_Jtvec diff --git a/simpeg/simulation.py b/simpeg/simulation.py index e1091b1997..f90d9e68a8 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -530,6 +530,30 @@ def make_synthetic_data( noise_floor=noise_floor, ) + @property + def store_sensitivities(self): + """Options for storing sensitivities. + + There are 3 options: + + - 'ram': sensitivity matrix stored in RAM + - 'disk': sensitivities written and stored to disk + - 'forward_only': sensitivities are not store (only use for forward simulation) + + Returns + ------- + {'disk', 'ram', 'forward_only'} + A string defining the model type for the simulation. + """ + if self._store_sensitivities is None: + self._store_sensitivities = "ram" + return self._store_sensitivities + + @store_sensitivities.setter + def store_sensitivities(self, value): + self._store_sensitivities = validate_string( + "store_sensitivities", value, ["disk", "ram", "forward_only"] + ) class BaseTimeSimulation(BaseSimulation): r"""Base class for time domain simulations. From 0948ee2f8e8c1043df4e1ef9a36f56bd1fab0327 Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 11 Jun 2024 10:57:31 -0700 Subject: [PATCH 443/455] Store Ainv on the class, fields only returns fields --- .../frequency_domain/simulation.py | 22 +++++++-------- .../static/induced_polarization/simulation.py | 13 ++++----- .../induced_polarization/simulation_2d.py | 15 +++++----- .../static/resistivity/simulation.py | 16 +++++------ .../static/resistivity/simulation_2d.py | 28 +++++++++---------- .../time_domain/simulation.py | 23 ++++++--------- simpeg/dask/simulation.py | 4 +-- 7 files changed, 55 insertions(+), 66 deletions(-) diff --git a/simpeg/dask/electromagnetics/frequency_domain/simulation.py b/simpeg/dask/electromagnetics/frequency_domain/simulation.py index 8e308b98cd..33d28d598b 100644 --- a/simpeg/dask/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/dask/electromagnetics/frequency_domain/simulation.py @@ -110,7 +110,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) all_receivers = [] @@ -137,7 +137,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): data = compute(array.hstack(rows))[0] if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data @@ -167,25 +167,25 @@ def fields(self, m=None, return_Ainv=False): Ainv_solve.clean() if return_Ainv: - return f, Ainv - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) - if len(Ainv) > 1: + if len(self.Ainv) > 1: raise NotImplementedError( "Current implementation of parallelization assumes a single frequency per simulation. " "Consider creating one misfit per frequency." ) - A_i = list(Ainv.values())[0] + A_i = list(self.Ainv.values())[0] m_size = self.model.size if self.store_sensitivities == "disk": @@ -227,13 +227,13 @@ def compute_J(self, f=None, Ainv=None): for block_derivs_chunks, addresses_chunks in tqdm( zip(blocks_receiver_derivs, blocks), ncols=len(blocks_receiver_derivs), - desc=f"Sensitivities at {list(Ainv)[0]} Hz", + desc=f"Sensitivities at {list(self.Ainv)[0]} Hz", ): Jmatrix = parallel_block_compute( self, Jmatrix, block_derivs_chunks, A_i, fields_array, addresses_chunks ) - for A in Ainv.values(): + for A in self.Ainv.values(): A.clean() if self.store_sensitivities == "disk": diff --git a/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py index a2d152e327..7dead77768 100644 --- a/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py @@ -34,7 +34,6 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._solutionType] = Ainv * RHS - Ainv.clean() if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) @@ -49,9 +48,9 @@ def dask_fields(self, m=None, return_Ainv=False): self._scale = scale.dobs if return_Ainv: - return f, self.solver(sp.csr_matrix(A.T), **self.solver_opts) - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields @@ -79,8 +78,8 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if self._Jmatrix is None or self._scale is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=True) - self._Jmatrix = self.compute_J(f=f, Ainv=Ainv) + f = self.fields(m, return_Ainv=True) + self._Jmatrix = self.compute_J(f=f) data = self.Jvec(m, m) @@ -98,7 +97,7 @@ def dask_getJtJdiag(self, m, W=None): Return the diagonal of JtJ """ self.model = m - if self._jtjdiag is None: + if getattr(self, "_jtjdiag", None) is None: if isinstance(self.Jmatrix, Future): self.Jmatrix # Wait to finish diff --git a/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py index cc4a21ee90..18a4a6cfbd 100644 --- a/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py @@ -31,15 +31,14 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f._quad_weights = self._quad_weights - Ainv_out = {} + Ainv = {} for iky, ky in enumerate(kys): A = self.getA(ky) - Ainv = self.solver(A, **self.solver_opts) - Ainv_out[iky] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) + Ainv[iky] = self.solver(A, **self.solver_opts) + RHS = self.getRHS(ky) - f[:, self._solutionType, iky] = Ainv * RHS + f[:, self._solutionType, iky] = Ainv[iky] * RHS - Ainv.clean() if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) @@ -55,9 +54,9 @@ def dask_fields(self, m=None, return_Ainv=False): self._scale = scale.dobs if return_Ainv: - return f, Ainv_out - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields diff --git a/simpeg/dask/electromagnetics/static/resistivity/simulation.py b/simpeg/dask/electromagnetics/static/resistivity/simulation.py index e63da34643..1205d57a1b 100644 --- a/simpeg/dask/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation.py @@ -30,21 +30,19 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._solutionType] = Ainv * RHS - Ainv.clean() - if return_Ainv: - return f, self.solver(sp.csr_matrix(A.T), **self.solver_opts) - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) m_size = self.model.size row_chunks = int( @@ -86,7 +84,7 @@ def compute_J(self, f=None, Ainv=None): df_duT, df_dmT = df_duTFun( source, None, PTv[:, start:end], adjoint=True ) - ATinvdf_duT = Ainv * df_duT + ATinvdf_duT = self.Ainv * df_duT dA_dmT = self.getADeriv(u_source, ATinvdf_duT, adjoint=True) dRHS_dmT = self.getRHSDeriv(source, ATinvdf_duT, adjoint=True) du_dmT = -dA_dmT @@ -130,7 +128,7 @@ def compute_J(self, f=None, Ainv=None): else: Jmatrix[count : self.survey.nD, :] = blocks.astype(np.float32) - Ainv.clean() + self.Ainv.clean() if self.store_sensitivities == "disk": del Jmatrix diff --git a/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py index 9457c1d56f..b11fb5c959 100644 --- a/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py @@ -27,31 +27,29 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f._quad_weights = self._quad_weights - Ainv_out = {} + Ainv = {} for iky, ky in enumerate(kys): A = self.getA(ky) - Ainv = self.solver(A, **self.solver_opts) - Ainv_out[iky] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) - RHS = self.getRHS(ky) - f[:, self._solutionType, iky] = Ainv * RHS + Ainv[iky] = self.solver(A, **self.solver_opts) - Ainv.clean() + RHS = self.getRHS(ky) + f[:, self._solutionType, iky] = Ainv[iky] * RHS if return_Ainv: - return f, Ainv_out - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): kys = self._quad_points weights = self._quad_weights if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) m_size = self.model.size row_chunks = int( @@ -99,7 +97,7 @@ def compute_J(self, f=None, Ainv=None): u_ky = f[:, self._solutionType, iky] u_source = u_ky[:, i_src] - ATinvdf_duT = Ainv[iky] * PTv[:, start:end] + ATinvdf_duT = self.Ainv[iky] * PTv[:, start:end] dA_dmT = self.getADeriv(ky, u_source, ATinvdf_duT, adjoint=True) du_dmT = -weights[iky] * dA_dmT block += du_dmT.T.reshape((-1, m_size)) @@ -135,7 +133,7 @@ def compute_J(self, f=None, Ainv=None): Jmatrix[count : self.survey.nD, :] = blocks.astype(np.float32) for iky, _ in enumerate(kys): - Ainv[iky].clean() + self.Ainv[iky].clean() if self.store_sensitivities == "disk": del Jmatrix @@ -176,7 +174,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) temp = np.empty(survey.nD) count = 0 @@ -187,7 +185,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): count += len(d) if compute_J: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return self._mini_survey_data(temp), Jmatrix return self._mini_survey_data(temp) diff --git a/simpeg/dask/electromagnetics/time_domain/simulation.py b/simpeg/dask/electromagnetics/time_domain/simulation.py index 03cc103f0f..915f8a7439 100644 --- a/simpeg/dask/electromagnetics/time_domain/simulation.py +++ b/simpeg/dask/electromagnetics/time_domain/simulation.py @@ -99,14 +99,11 @@ def fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._fieldType + "Solution", 0] = self.getInitialFields() Ainv = {} - ATinv = {} for tInd, dt in enumerate(self.time_steps): if dt not in Ainv: A = self.getAdiag(tInd) Ainv[dt] = self.solver(sp.csr_matrix(A), **self.solver_opts) - if return_Ainv: - ATinv[dt] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) Asubdiag = self.getAsubdiag(tInd) rhs = -Asubdiag * f[:, (self._fieldType + "Solution"), tInd] @@ -120,13 +117,11 @@ def fields(self, m=None, return_Ainv=False): sol = Ainv[dt] * rhs f[:, self._fieldType + "Solution", tInd + 1] = sol - for A in Ainv.values(): - A.clean() if return_Ainv: - return f, ATinv - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = fields @@ -208,7 +203,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) rows = [] receiver_projection = self.survey.source_list[0].receiver_list[0].projField @@ -241,7 +236,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): data = array.hstack(rows).compute() if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data @@ -502,12 +497,12 @@ def compute_rows( return np.vstack(rows) -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): """ Compute the rows for the sensitivity matrix. """ if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) ftype = self._fieldType + "Solution" sens_name = self.sensitivity_path[:-5] @@ -542,7 +537,7 @@ def compute_J(self, f=None, Ainv=None): ATinv_df_duT_v = {} for tInd, dt in tqdm(zip(reversed(range(self.nT)), reversed(self.time_steps))): - AdiagTinv = Ainv[dt] + AdiagTinv = self.Ainv[dt] j_row_updates = [] time_mask = data_times > simulation_times[tInd] @@ -587,7 +582,7 @@ def compute_J(self, f=None, Ainv=None): else: Jmatrix += array.vstack(j_row_updates).compute() - for A in Ainv.values(): + for A in self.Ainv.values(): A.clean() if self.store_sensitivities == "ram": diff --git a/simpeg/dask/simulation.py b/simpeg/dask/simulation.py index 342d5f2e74..3c78f9e98e 100644 --- a/simpeg/dask/simulation.py +++ b/simpeg/dask/simulation.py @@ -218,7 +218,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) def evaluate_receiver(source, receiver, mesh, fields): return receiver.eval(source, mesh, fields).flatten() @@ -238,7 +238,7 @@ def evaluate_receiver(source, receiver, mesh, fields): data = array.hstack(rows).compute() if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data From 636fd9f2c93fd379c53dc3c49f2d7d01dc3d1bf4 Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 11 Jun 2024 11:08:07 -0700 Subject: [PATCH 444/455] Update cross-gradient --- simpeg/directives/directives.py | 11 ++-- simpeg/maps.py | 37 ++++++++--- simpeg/regularization/cross_gradient.py | 82 +++++++++++++++++++------ simpeg/simulation.py | 1 + 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index d58e04c59d..e184c17248 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -49,7 +49,6 @@ validate_ndarray_with_shape, ) -from geoh5py.workspace import Workspace from geoh5py.objects import ObjectBase from geoh5py.ui_json.utils import fetch_active_workspace @@ -3312,7 +3311,7 @@ def target(self): for survey in self.survey: nD += survey.nD - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -3374,9 +3373,13 @@ def endIter(self): multipliers = [] for mult, reg in self.reg: if isinstance(reg, CrossGradient): - for wire in reg.wire_map: + units = [] + for _, wire in reg.wire_map.maps: if wire in angle_map: - mult = 0 + units.append("radian") + else: + units.append("metric") + multipliers.append(mult) diff --git a/simpeg/maps.py b/simpeg/maps.py index 09ef514cb2..1e161836cb 100644 --- a/simpeg/maps.py +++ b/simpeg/maps.py @@ -1317,23 +1317,30 @@ def __init__(self, *args): and isinstance(arg[0], str) and # TODO: this should be extended to a slice. - isinstance(arg[1], (int, np.integer)) + isinstance(arg[1], (int, np.integer, Projection)) ), ( - "Each wire needs to be a tuple: (name, length). " + "Each wire needs to be a tuple: (name, length) or (name, Projection). " "You provided: {}".format(arg) ) - self._nP = int(np.sum([w[1] for w in args])) start = 0 maps = [] for arg in args: - wire = Projection(self.nP, slice(start, start + arg[1])) + + if isinstance(arg[1], (int, np.integer)): + wire = Projection(self.nP, slice(start, start + arg[1])) + start += arg[1] + else: + wire = arg[1] + setattr(self, arg[0], wire) maps += [(arg[0], wire)] - start += arg[1] - self.maps = maps - self._tuple = namedtuple("Model", [w[0] for w in args]) + + self.maps = maps + self._nP = maps[0][1].nP + self._tuple = namedtuple("Model", [name for name, _ in args]) + self._projection = sp.vstack([wire.P for _, wire in self.maps]) def __mul__(self, val): assert isinstance(val, np.ndarray) @@ -1353,6 +1360,22 @@ def nP(self): """ return self._nP + def deriv(self, m): + """ + Derivative of the mapping with respect to the input parameters + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The Gradient of the mapping function evaluated for the model provided. + """ + return self._projection + class SelfConsistentEffectiveMedium(IdentityMap): r""" diff --git a/simpeg/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py index bf9353d652..bda963a343 100644 --- a/simpeg/regularization/cross_gradient.py +++ b/simpeg/regularization/cross_gradient.py @@ -130,8 +130,9 @@ class CrossGradient(BaseSimilarityMeasure): """ - def __init__(self, mesh, wire_map, approx_hessian=True, **kwargs): - super().__init__(mesh, wire_map=wire_map, **kwargs) + def __init__(self, mesh, wire_map, approx_hessian=True, units=["metric", "metric"], **kwargs): + + super().__init__(mesh, wire_map=wire_map, units=units, **kwargs) self.approx_hessian = approx_hessian regmesh = self.regularization_mesh @@ -227,6 +228,36 @@ def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): return cross_prod + def _model_gradients(self, models): + """ + Compute gradient on faces + """ + gradients = [] + + for unit, (name, wire) in zip(self.units, self.wire_map.maps): + model = wire * models + if unit == "radian": + gradient = [] + components = "xyz" if self.regularization_mesh.dim == 3 else "xy" + for comp in components: + distances = getattr( + self.regularization_mesh, f"cell_distances_{comp}" + ) + cell_grad = getattr( + self.regularization_mesh, f"cell_gradient_{comp}" + ) + gradient.append( + coterminal(cell_grad * model * distances) / distances + ) + + gradient = np.hstack(gradient) / np.pi + else: + gradient = self._G @ model + + gradients.append(gradient) + + return gradients + def __call__(self, model): """Evaluate the cross-gradient regularization function for the model provided. @@ -244,11 +275,10 @@ def __call__(self, model): The regularization function evaluated for the model provided. """ - m1, m2 = self.wire_map * model Av = self._Av - G = self._G - g_m1 = G @ m1 - g_m2 = G @ m2 + + g_m1, g_m2 = self._model_gradients(model) + return np.sum((Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2) def deriv(self, model): @@ -279,14 +309,11 @@ def deriv(self, model): (n_param, ) numpy.ndarray Gradient of the regularization function evaluated for the model provided. """ - m1, m2 = self.wire_map * model - Av = self._Av G = self._G - g_m1 = G @ m1 - g_m2 = G @ m2 + g_m1, g_m2 = self._model_gradients(model) - return ( + return self.wire_map.deriv(model).T * ( 2 * np.r_[ (((Av @ g_m2**2) @ Av) * g_m1) @ G @@ -337,13 +364,10 @@ def deriv2(self, model, v=None): for the models provided is returned. If *v* is not ``None``, the Hessian multiplied by the vector provided is returned. """ - m1, m2 = self.wire_map * model - Av = self._Av G = self._G - g_m1 = G @ m1 - g_m2 = G @ m2 + g_m1, g_m2 = self._model_gradients(model) d11_mid = Av.T @ (Av @ g_m2**2) d12_mid = -(Av.T @ (Av @ (g_m1 * g_m2))) @@ -365,9 +389,9 @@ def deriv2(self, model, v=None): D12 = G.T @ D12_mid @ G D22 = G.T @ D22_mid @ G - return 2 * sp.bmat( + return 2 * self.wire_map.deriv(model).T * sp.bmat( [[D11, D12], [D12.T, D22]], format="csr" - ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + ) * self.wire_map.deriv(model) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 else: v1, v2 = self.wire_map * v @@ -389,5 +413,27 @@ def deriv2(self, model, v=None): - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued ) return ( - 2 * np.r_[p1, p2] + 2 * self.wire_map.deriv(model).T * np.r_[p1, p2] ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + + @property + def units(self) -> list[str] | None: + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ + return self._units + + @units.setter + def units(self, units: list[str] | None): + if units is not None and not isinstance(units, list) and not all(isinstance(u, str) for u in units): + raise TypeError( + f"'units' must be None or a list of str. " + f"Value of type {type(units)} provided." + ) + self._units = units \ No newline at end of file diff --git a/simpeg/simulation.py b/simpeg/simulation.py index f90d9e68a8..14dbdd90b0 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -89,6 +89,7 @@ def __init__( verbose=False, **kwargs, ): + self._store_sensitivities: str | None = None self.mesh = mesh self.survey = survey if solver is None: From 1b81d6a1214f4a3d9a2d112809dcdf5787967e2e Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 11 Jun 2024 12:50:46 -0700 Subject: [PATCH 445/455] Fix radian update. Default to no-cross on angles --- simpeg/directives/directives.py | 6 ++++-- simpeg/regularization/cross_gradient.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index e184c17248..af56eb685b 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -3282,7 +3282,7 @@ def association(self, value): class VectorInversion(InversionDirective): """ - Control a vector inversion from Cartesian to spherical coordinates + Control a vector inversion from Cartesian to spherical coordinates. """ chifact_target = 1.0 @@ -3369,7 +3369,7 @@ def endIter(self): else: reg_fun.units = "amplitude" - # Turn of cross-gradient on angles + # Change units of cross-gradient on angles multipliers = [] for mult, reg in self.reg: if isinstance(reg, CrossGradient): @@ -3377,9 +3377,11 @@ def endIter(self): for _, wire in reg.wire_map.maps: if wire in angle_map: units.append("radian") + mult = 0 # TODO Make this optional else: units.append("metric") + reg.units = units multipliers.append(mult) diff --git a/simpeg/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py index bda963a343..91c6ae075e 100644 --- a/simpeg/regularization/cross_gradient.py +++ b/simpeg/regularization/cross_gradient.py @@ -2,7 +2,7 @@ import scipy.sparse as sp from .base import BaseSimilarityMeasure -from ..utils import validate_type +from ..utils import validate_type, coterminal ############################################################################### From c2f34dde92739becac20fc02f4b4959955aac1f9 Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 11 Jun 2024 13:01:26 -0700 Subject: [PATCH 446/455] Black and flake8 --- .../static/induced_polarization/simulation.py | 3 --- .../induced_polarization/simulation_2d.py | 3 --- .../static/resistivity/simulation.py | 1 - .../static/resistivity/simulation_2d.py | 2 -- .../time_domain/simulation.py | 1 - simpeg/dask/potential_fields/base.py | 2 ++ .../potential_fields/gravity/simulation.py | 1 + .../potential_fields/magnetics/simulation.py | 1 + simpeg/data_misfit.py | 14 ++++++++----- simpeg/directives/directives.py | 2 -- simpeg/maps.py | 1 - simpeg/regularization/cross_gradient.py | 21 +++++++++++++------ simpeg/simulation.py | 1 + 13 files changed, 29 insertions(+), 24 deletions(-) diff --git a/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py index 7dead77768..81f2db5a0b 100644 --- a/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py @@ -1,5 +1,3 @@ -import scipy.sparse as sp - from .....electromagnetics.static.induced_polarization.simulation import ( BaseIPSimulation as Sim, ) @@ -34,7 +32,6 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._solutionType] = Ainv * RHS - if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) # loop through receivers to check if they need to set the _dc_voltage diff --git a/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py index 18a4a6cfbd..963fc12451 100644 --- a/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py @@ -1,5 +1,3 @@ -import scipy.sparse as sp - from .....electromagnetics.static.induced_polarization.simulation import ( Simulation2DNodal as Sim, ) @@ -39,7 +37,6 @@ def dask_fields(self, m=None, return_Ainv=False): RHS = self.getRHS(ky) f[:, self._solutionType, iky] = Ainv[iky] * RHS - if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) f_fwd = self.fields_to_space(f) diff --git a/simpeg/dask/electromagnetics/static/resistivity/simulation.py b/simpeg/dask/electromagnetics/static/resistivity/simulation.py index 1205d57a1b..d82a0f2198 100644 --- a/simpeg/dask/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation.py @@ -3,7 +3,6 @@ from .....utils import Zero import dask.array as da import numpy as np -import scipy.sparse as sp import zarr import numcodecs diff --git a/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py index b11fb5c959..08b5ba08de 100644 --- a/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py @@ -1,5 +1,3 @@ -import scipy.sparse as sp - from .....electromagnetics.static.resistivity.simulation_2d import ( BaseDCSimulation2D as Sim, ) diff --git a/simpeg/dask/electromagnetics/time_domain/simulation.py b/simpeg/dask/electromagnetics/time_domain/simulation.py index 915f8a7439..f6300feab1 100644 --- a/simpeg/dask/electromagnetics/time_domain/simulation.py +++ b/simpeg/dask/electromagnetics/time_domain/simulation.py @@ -117,7 +117,6 @@ def fields(self, m=None, return_Ainv=False): sol = Ainv[dt] * rhs f[:, self._fieldType + "Solution", tInd + 1] = sol - if return_Ainv: self.Ainv = Ainv diff --git a/simpeg/dask/potential_fields/base.py b/simpeg/dask/potential_fields/base.py index 58ae806431..3c5d05aedd 100644 --- a/simpeg/dask/potential_fields/base.py +++ b/simpeg/dask/potential_fields/base.py @@ -113,7 +113,9 @@ def dask_linear_operator(self): Sim.linear_operator = dask_linear_operator + def compute_J(self): return self.linear_operator() + Sim.compute_J = compute_J diff --git a/simpeg/dask/potential_fields/gravity/simulation.py b/simpeg/dask/potential_fields/gravity/simulation.py index 90f58acccf..780e37057a 100644 --- a/simpeg/dask/potential_fields/gravity/simulation.py +++ b/simpeg/dask/potential_fields/gravity/simulation.py @@ -2,6 +2,7 @@ from ....potential_fields.gravity import Simulation3DIntegral as Sim from ....utils import sdiag, mkvc + def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ diff --git a/simpeg/dask/potential_fields/magnetics/simulation.py b/simpeg/dask/potential_fields/magnetics/simulation.py index 9241894ce8..5682066d2f 100644 --- a/simpeg/dask/potential_fields/magnetics/simulation.py +++ b/simpeg/dask/potential_fields/magnetics/simulation.py @@ -2,6 +2,7 @@ from ....potential_fields.magnetics import Simulation3DIntegral as Sim from ....utils import sdiag, mkvc + def dask_getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index fbbd41b444..b796f78c21 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -36,7 +36,9 @@ class inherits the :py:class:`simpeg.objective_function.L2ObjectiveFunction`. Assign a SimPEG ``Counter`` object to store iterations and run-times. """ - def __init__(self, data, simulation, model_map=None, debug=False, counter=None, **kwargs): + def __init__( + self, data, simulation, model_map=None, debug=False, counter=None, **kwargs + ): super().__init__(has_fields=True, debug=debug, counter=counter, **kwargs) self.data = data @@ -353,8 +355,8 @@ def getJtJdiag(self, m): """ if getattr(self.simulation, "getJtJdiag", None) is None: raise AttributeError( - "Simulation does not have a getJtJdiag attribute." - + "Cannot form the sensitivity explicitly" + "Simulation does not have a getJtJdiag attribute." + + "Cannot form the sensitivity explicitly" ) mapping_deriv = self.model_map.deriv(m) @@ -365,6 +367,8 @@ def getJtJdiag(self, m): jtjdiag = self.simulation.getJtJdiag(m, W=self.W) if self.model_map is not None: - jtjdiag = mkvc((sdiag(np.sqrt(jtjdiag)) @ mapping_deriv).power(2).sum(axis=0)) + jtjdiag = mkvc( + (sdiag(np.sqrt(jtjdiag)) @ mapping_deriv).power(2).sum(axis=0) + ) - return jtjdiag \ No newline at end of file + return jtjdiag diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index af56eb685b..20997425a9 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -53,7 +53,6 @@ from geoh5py.ui_json.utils import fetch_active_workspace - class InversionDirective: """Base inversion directive class. @@ -3279,7 +3278,6 @@ def association(self, value): self._association = value.upper() - class VectorInversion(InversionDirective): """ Control a vector inversion from Cartesian to spherical coordinates. diff --git a/simpeg/maps.py b/simpeg/maps.py index 1e161836cb..3361fe725a 100644 --- a/simpeg/maps.py +++ b/simpeg/maps.py @@ -1336,7 +1336,6 @@ def __init__(self, *args): setattr(self, arg[0], wire) maps += [(arg[0], wire)] - self.maps = maps self._nP = maps[0][1].nP self._tuple = namedtuple("Model", [name for name, _ in args]) diff --git a/simpeg/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py index 91c6ae075e..7632f26a5f 100644 --- a/simpeg/regularization/cross_gradient.py +++ b/simpeg/regularization/cross_gradient.py @@ -130,7 +130,9 @@ class CrossGradient(BaseSimilarityMeasure): """ - def __init__(self, mesh, wire_map, approx_hessian=True, units=["metric", "metric"], **kwargs): + def __init__( + self, mesh, wire_map, approx_hessian=True, units=["metric", "metric"], **kwargs + ): super().__init__(mesh, wire_map=wire_map, units=units, **kwargs) self.approx_hessian = approx_hessian @@ -389,9 +391,12 @@ def deriv2(self, model, v=None): D12 = G.T @ D12_mid @ G D22 = G.T @ D22_mid @ G - return 2 * self.wire_map.deriv(model).T * sp.bmat( - [[D11, D12], [D12.T, D22]], format="csr" - ) * self.wire_map.deriv(model) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + return ( + 2 + * self.wire_map.deriv(model).T + * sp.bmat([[D11, D12], [D12.T, D22]], format="csr") + * self.wire_map.deriv(model) + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 else: v1, v2 = self.wire_map * v @@ -431,9 +436,13 @@ def units(self) -> list[str] | None: @units.setter def units(self, units: list[str] | None): - if units is not None and not isinstance(units, list) and not all(isinstance(u, str) for u in units): + if ( + units is not None + and not isinstance(units, list) + and not all(isinstance(u, str) for u in units) + ): raise TypeError( f"'units' must be None or a list of str. " f"Value of type {type(units)} provided." ) - self._units = units \ No newline at end of file + self._units = units diff --git a/simpeg/simulation.py b/simpeg/simulation.py index 14dbdd90b0..0158205bb3 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -556,6 +556,7 @@ def store_sensitivities(self, value): "store_sensitivities", value, ["disk", "ram", "forward_only"] ) + class BaseTimeSimulation(BaseSimulation): r"""Base class for time domain simulations. From 6cefa5ed740c931c4060e4c30ff34a059de67fff Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 11 Jun 2024 15:10:38 -0700 Subject: [PATCH 447/455] Change to lower case name --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a8193e5a2e..309bfd5e54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ ] packages = [ - { include = "SimPEG" }, + { include = "simpeg" }, ] exclude = ["tests*", "examples*", "tutorials*"] From 1d96479ea7e5cf6ae6e089ea7dff317c8d4ac87d Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 12 Jun 2024 09:58:23 -0700 Subject: [PATCH 448/455] Delete SimPEG directory --- SimPEG/dask/__init__.py | 19 - SimPEG/data_misfit.py | 335 --- SimPEG/directives/__init__.py | 134 - .../frequency_domain/simulation.py | 2182 ---------------- SimPEG/potential_fields/gravity/simulation.py | 514 ---- SimPEG/regularization/base.py | 2300 ----------------- SimPEG/regularization/cross_gradient.py | 393 --- SimPEG/regularization/sparse.py | 1095 -------- SimPEG/simulation.py | 1053 -------- 9 files changed, 8025 deletions(-) delete mode 100644 SimPEG/dask/__init__.py delete mode 100644 SimPEG/data_misfit.py delete mode 100644 SimPEG/directives/__init__.py delete mode 100644 SimPEG/electromagnetics/frequency_domain/simulation.py delete mode 100644 SimPEG/potential_fields/gravity/simulation.py delete mode 100644 SimPEG/regularization/base.py delete mode 100644 SimPEG/regularization/cross_gradient.py delete mode 100644 SimPEG/regularization/sparse.py delete mode 100644 SimPEG/simulation.py diff --git a/SimPEG/dask/__init__.py b/SimPEG/dask/__init__.py deleted file mode 100644 index f5a00b7334..0000000000 --- a/SimPEG/dask/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -try: - import simpeg.dask.simulation - import simpeg.dask.electromagnetics.frequency_domain.simulation - import simpeg.dask.electromagnetics.static.resistivity.simulation - import simpeg.dask.electromagnetics.static.resistivity.simulation_2d - import simpeg.dask.electromagnetics.static.induced_polarization.simulation - import simpeg.dask.electromagnetics.static.induced_polarization.simulation_2d - import simpeg.dask.electromagnetics.time_domain.simulation - import simpeg.dask.potential_fields.base - import simpeg.dask.potential_fields.gravity.simulation - import simpeg.dask.potential_fields.magnetics.simulation - import simpeg.dask.simulation - import simpeg.dask.data_misfit - import simpeg.dask.inverse_problem - import simpeg.dask.objective_function - -except ImportError as err: - print("unable to load dask operations") - print(err) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py deleted file mode 100644 index 6b489b425a..0000000000 --- a/SimPEG/data_misfit.py +++ /dev/null @@ -1,335 +0,0 @@ -import numpy as np -from .utils import Counter, sdiag, timeIt, Identity, validate_type -from .data import Data -from .simulation import BaseSimulation -from .objective_function import L2ObjectiveFunction - -__all__ = ["L2DataMisfit"] - - -class BaseDataMisfit(L2ObjectiveFunction): - r"""Base data misfit class. - - Inherit this class to build your own data misfit function. The ``BaseDataMisfit`` - class inherits the :py:class:`simpeg.objective_function.L2ObjectiveFunction`. - And as a result, it is limited to building data misfit functions of the form: - - .. important:: - This class is not meant to be instantiated. You should inherit from it to - create your own data misfit class. - - .. math:: - \phi_d (\mathbf{m}) = \| \mathbf{W} f(\mathbf{m}) \|_2^2 - - where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{W}` is a linear weighting - matrix, and :math:`f` is a mapping function that acts on the model. - - Parameters - ---------- - data : simpeg.data.Data - A SimPEG data object. - simulation : simpeg.simulation.BaseSimulation - A SimPEG simulation object. - debug : bool - Print debugging information. - counter : None or simpeg.utils.Counter - Assign a SimPEG ``Counter`` object to store iterations and run-times. - """ - - def __init__(self, data, simulation, debug=False, counter=None, **kwargs): - super().__init__(has_fields=True, debug=debug, counter=counter, **kwargs) - - self.data = data - self.simulation = simulation - - @property - def data(self): - """A SimPEG data object. - - Returns - ------- - simpeg.data.Data - A SimPEG data object. - """ - return self._data - - @data.setter - def data(self, value): - self._data = validate_type("data", value, Data, cast=False) - - @property - def simulation(self): - """A SimPEG simulation object. - - Returns - ------- - simpeg.simulation.BaseSimulation - A SimPEG simulation object. - """ - return self._simulation - - @simulation.setter - def simulation(self, value): - self._simulation = validate_type( - "simulation", value, BaseSimulation, cast=False - ) - - @property - def debug(self): - """Print debugging information. - - Returns - ------- - bool - Print debugging information. - """ - return self._debug - - @debug.setter - def debug(self, value): - self._debug = validate_type("debug", value, bool) - - @property - def counter(self): - """SimPEG ``Counter`` object to store iterations and run-times. - - Returns - ------- - None or simpeg.utils.Counter - SimPEG ``Counter`` object to store iterations and run-times. - """ - return self._counter - - @counter.setter - def counter(self, value): - if value is not None: - value = validate_type("counter", value, Counter, cast=False) - - @property - def nP(self): - """Number of model parameters. - - Returns - ------- - int - Number of model parameters. - """ - if self._mapping is not None: - return self.mapping.nP - elif self.simulation.model is not None: - return len(self.simulation.model) - else: - return "*" - - @property - def nD(self): - """Number of data. - - Returns - ------- - int - Number of data. - """ - return self.data.nD - - @property - def shape(self): - """Shape of the Jacobian. - - The number of data by the number of model parameters. - - Returns - ------- - tuple of int (n_data, n_param) - Shape of the Jacobian; i.e. number of data by the number of model parameters. - """ - return (self.nD, self.nP) - - @property - def W(self): - r"""The data weighting matrix. - - For a discrete least-squares data misfit function of the form: - - .. math:: - \phi_d (\mathbf{m}) = \| \mathbf{W} \mathbf{f}(\mathbf{m}) \|_2^2 - - :math:`\mathbf{W}` is a linear weighting matrix, :math:`\mathbf{m}` is the model vector, - and :math:`\mathbf{f}` is a discrete mapping function that acts on the model vector. - - Returns - ------- - scipy.sparse.csr_matrix - The data weighting matrix. - """ - - if getattr(self, "_W", None) is None: - if self.data is None: - raise Exception( - "data with standard deviations must be set before the data " - "misfit can be constructed. Please set the data: " - "dmis.data = Data(dobs=dobs, relative_error=rel" - ", noise_floor=eps)" - ) - standard_deviation = self.data.standard_deviation - if standard_deviation is None: - raise Exception( - "data standard deviations must be set before the data misfit " - "can be constructed (data.relative_error = 0.05, " - "data.noise_floor = 1e-5), alternatively, the W matrix " - "can be set directly (dmisfit.W = 1./standard_deviation)" - ) - if any(standard_deviation <= 0): - raise Exception( - "data.standard_deviation must be strictly positive to construct " - "the W matrix. Please set data.relative_error and or " - "data.noise_floor." - ) - self._W = sdiag(1 / (standard_deviation)) - return self._W - - @W.setter - def W(self, value): - if isinstance(value, Identity): - value = np.ones(self.data.nD) - if len(value.shape) < 2: - value = sdiag(value) - assert value.shape == ( - self.data.nD, - self.data.nD, - ), "W must have shape ({nD},{nD}), not ({val0}, {val1})".format( - nD=self.data.nD, val0=value.shape[0], val1=value.shape[1] - ) - self._W = value - - def residual(self, m, f=None): - r"""Computes the data residual vector for a given model. - - Where :math:`\mathbf{d}_\text{obs}` is the observed data vector and :math:`\mathbf{d}_\text{pred}` - is the predicted data vector for a model vector :math:`\mathbf{m}`, this function - computes the data residual: - - .. math:: - \mathbf{r} = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the function is evaluated. - f : None or simpeg.fields.Fields, optional - A SimPEG fields object. Used when the fields for the model *m* have - already been computed. - - Returns - ------- - (n_data, ) numpy.ndarray - The data residual vector. - """ - if self.data is None: - raise Exception("data must be set before a residual can be calculated.") - return self.simulation.residual(m, self.data.dobs, f=f) - - -class L2DataMisfit(BaseDataMisfit): - r"""Least-squares data misfit. - - Define the data misfit as the L2-norm of the weighted residual between observed - data and predicted data for a given model. I.e.: - - .. math:: - \phi_d (\mathbf{m}) = \big \| \mathbf{W_d} - \big ( \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} \big ) \big \|_2^2 - - where :math:`\mathbf{d}_\text{obs}` is the observed data vector, :math:`\mathbf{d}_\text{pred}` - is the predicted data vector for a model vector :math:`\mathbf{m}`, and - :math:`\mathbf{W_d}` is the data weighting matrix. The diagonal elements of - :math:`\mathbf{W_d}` are the reciprocals of the data uncertainties - :math:`\boldsymbol{\varepsilon}`. Thus: - - .. math:: - \mathbf{W_d} = \text{diag} \left ( \boldsymbol{\varepsilon}^{-1} \right ) - - Parameters - ---------- - data : simpeg.data.Data - A SimPEG data object that has observed data and uncertainties. - simulation : simpeg.simulation.BaseSimulation - A SimPEG simulation object. - debug : bool - Print debugging information. - counter : None or simpeg.utils.Counter - Assign a SimPEG ``Counter`` object to store iterations and run-times. - """ - - @timeIt - def __call__(self, m, f=None): - """Evaluate the residual for a given model.""" - - R = self.W * self.residual(m, f=f) - return np.vdot(R, R) - - @timeIt - def deriv(self, m, f=None): - r"""Gradient of the data misfit function evaluated for the model provided. - - Where :math:`\phi_d (\mathbf{m})` is the data misfit function, - this method evaluates and returns the derivative with respect to the model parameters; i.e. - the gradient: - - .. math:: - \frac{\partial \phi_d}{\partial \mathbf{m}} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the gradient is evaluated. - - Returns - ------- - (n_param, ) numpy.ndarray - The gradient of the data misfit function evaluated for the model provided. - """ - - if f is None: - f = self.simulation.fields(m) - - return 2 * self.simulation.Jtvec( - m, self.W.T * (self.W * self.residual(m, f=f)), f=f - ) - - @timeIt - def deriv2(self, m, v, f=None): - r"""Hessian of the data misfit function evaluated for the model provided. - - Where :math:`\phi_d (\mathbf{m})` is the data misfit function, - this method returns the second-derivative (Hessian) with respect to the model parameters: - - .. math:: - \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} - - or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: - - .. math:: - \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} \, \mathbf{v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the Hessian is evaluated. - v : None or (n_param, ) numpy.ndarray, optional - A vector. - - Returns - ------- - (n_param, n_param) scipy.sparse.csr_matrix or (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the Hessian of the data misfit - function for the model provided is returned. If *v* is not ``None``, - the Hessian multiplied by the vector provided is returned. - """ - - if f is None: - f = self.simulation.fields(m) - - return 2 * self.simulation.Jtvec_approx( - m, self.W * (self.W * self.simulation.Jvec_approx(m, v, f=f)), f=f - ) diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py deleted file mode 100644 index 69081767d1..0000000000 --- a/SimPEG/directives/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -============================================= -Directives (:mod:`simpeg.directives`) -============================================= - -.. currentmodule:: simpeg.directives - -Directives are classes that allow us to control the inversion, perform tasks -between iterations, save information about our inversion process and more. -Directives are passed to the ``simpeg.inversion.BaseInversion`` class through -the ``directiveList`` argument. The tasks specified through the directives are -executed after each inversion iteration, following the same order as in which -they are passed in the ``directiveList``. - -Although you can write your own directive classes and plug them into your -inversion, we provide a set of useful directive classes that cover a wide range -of applications: - - -General purpose directives -========================== - -.. autosummary:: - :toctree: generated/ - - AlphasSmoothEstimate_ByEig - BetaEstimateMaxDerivative - BetaEstimate_ByEig - BetaSchedule - JointScalingSchedule - MultiTargetMisfits - ProjectSphericalBounds - ScalingMultipleDataMisfits_ByEig - TargetMisfit - UpdatePreconditioner - UpdateSensitivityWeights - Update_Wj - - -Directives to save inversion results -==================================== - -.. autosummary:: - :toctree: generated/ - - SaveEveryIteration - SaveModelEveryIteration - SaveOutputDictEveryIteration - SaveOutputEveryIteration - - -Directives related to sparse inversions -======================================= - -.. autosummary:: - :toctree: generated/ - - Update_IRLS - - -Directives related to PGI -========================= - -.. autosummary:: - :toctree: generated/ - - PGI_AddMrefInSmooth - PGI_BetaAlphaSchedule - PGI_UpdateParameters - - -Directives related to joint inversions -====================================== - -.. autosummary:: - :toctree: generated/ - - SimilarityMeasureInversionDirective - SimilarityMeasureSaveOutputEveryIteration - PairedBetaEstimate_ByEig - PairedBetaSchedule - MovingAndMultiTargetStopping - - -Base directive classes -====================== -The ``InversionDirective`` class defines the basic class for all directives. -Inherit from this class when writing your own directive. The ``DirectiveList`` -is used under the hood to handle the execution of all directives passed to the -``simpeg.inversion.BaseInversion``. - -.. autosummary:: - :toctree: generated/ - - InversionDirective - DirectiveList - -""" - -from .directives import ( - InversionDirective, - DirectiveList, - BetaEstimateMaxDerivative, - BetaEstimate_ByEig, - BetaSchedule, - TargetMisfit, - SaveEveryIteration, - SaveModelEveryIteration, - SaveOutputEveryIteration, - SaveOutputDictEveryIteration, - Update_IRLS, - UpdatePreconditioner, - Update_Wj, - AlphasSmoothEstimate_ByEig, - MultiTargetMisfits, - ScalingMultipleDataMisfits_ByEig, - JointScalingSchedule, - UpdateSensitivityWeights, - ProjectSphericalBounds, -) - -from .pgi_directives import ( - PGI_UpdateParameters, - PGI_BetaAlphaSchedule, - PGI_AddMrefInSmooth, -) - -from .sim_directives import ( - SimilarityMeasureInversionDirective, - SimilarityMeasureSaveOutputEveryIteration, - PairedBetaEstimate_ByEig, - PairedBetaSchedule, - MovingAndMultiTargetStopping, -) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py deleted file mode 100644 index de1afecb68..0000000000 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ /dev/null @@ -1,2182 +0,0 @@ -import numpy as np -import scipy.sparse as sp -from discretize.utils import Zero - -from ... import props -from ...data import Data -from ...utils import mkvc, validate_type -from ..base import BaseEMSimulation -from ..utils import omega -from .survey import Survey -from .fields import ( - FieldsFDEM, - Fields3DElectricField, - Fields3DMagneticFluxDensity, - Fields3DMagneticField, - Fields3DCurrentDensity, -) - -import warnings - - -class BaseFDEMSimulation(BaseEMSimulation): - r"""Base finite volume FDEM simulation class. - - This class is used to define properties and methods necessary for solving - 3D frequency-domain EM problems. For a :math:`+i\omega t` Fourier convention, - Maxwell's equations are expressed as: - - .. math:: - \begin{align} - \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ - \nabla \times \vec{H} - \vec{J} &= \vec{S}_e - \end{align} - - where the constitutive relations between fields and fluxes are given by: - - * :math:`\vec{J} = \sigma \vec{E}` - * :math:`\vec{B} = \mu \vec{H}` - - and: - - * :math:`\vec{S}_m` represents a magnetic source term - * :math:`\vec{S}_e` represents a current source term - - Child classes of ``BaseFDEMSimulation`` solve the above expression numerically - for various cases using mimetic finite volume. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh. - survey : .frequency_domain.survey.Survey - The frequency-domain EM survey. - forward_only : bool, optional - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - permittivity : (n_cells,) numpy.ndarray, optional - Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement - is ignored. Please note that `permittivity` is not an invertible property, and that future - development will result in the deprecation of this propery. - storeJ : bool, optional - Whether to compute and store the sensitivity matrix. - """ - - fieldsPair = FieldsFDEM - permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - - def __init__( - self, - mesh, - survey=None, - forward_only=False, - permittivity=None, - storeJ=False, - **kwargs - ): - super().__init__(mesh=mesh, survey=survey, **kwargs) - self.forward_only = forward_only - if permittivity is not None: - warnings.warn( - "Simulations using permittivity have not yet been thoroughly tested and derivatives are not implemented. Contributions welcome!", - stacklevel=2, - ) - self.permittivity = permittivity - self.storeJ = storeJ - - @property - def survey(self): - """The FDEM survey object. - - Returns - ------- - .frequency_domain.survey.Survey - The FDEM survey object. - """ - if self._survey is None: - raise AttributeError("Simulation must have a survey set") - return self._survey - - @survey.setter - def survey(self, value): - if value is not None: - value = validate_type("survey", value, Survey, cast=False) - self._survey = value - self._survey = value - - @property - def storeJ(self): - """Whether to compute and store the sensitivity matrix. - - Returns - ------- - bool - Whether to compute and store the sensitivity matrix. - """ - return self._storeJ - - @storeJ.setter - def storeJ(self, value): - self._storeJ = validate_type("storeJ", value, bool) - - @property - def forward_only(self): - """Whether to store the factorizations of the inverses of the system matrices. - - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - - Returns - ------- - bool - Whether to store the factorizations of the inverses of the system matrices. - """ - return self._forward_only - - @forward_only.setter - def forward_only(self, value): - self._forward_only = validate_type("forward_only", value, bool) - - def _get_admittivity(self, freq): - if self.permittivity is not None: - return self.sigma + 1j * self.permittivity * omega(freq) - else: - return self.sigma - - def _get_face_admittivity_property_matrix( - self, freq, invert_model=False, invert_matrix=False - ): - """ - Face inner product matrix with permittivity and resistivity - """ - yhat = self._get_admittivity(freq) - return self.mesh.get_face_inner_product( - yhat, invert_model=invert_model, invert_matrix=invert_matrix - ) - - def _get_edge_admittivity_property_matrix( - self, freq, invert_model=False, invert_matrix=False - ): - """ - Face inner product matrix with permittivity and resistivity - """ - yhat = self._get_admittivity(freq) - return self.mesh.get_edge_inner_product( - yhat, invert_model=invert_model, invert_matrix=invert_matrix - ) - - # @profile - def fields(self, m=None): - """Compute and return the fields for the model provided. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model. - - Returns - ------- - .frequency_domain.fields.FieldsFDEM - The FDEM fields object. - """ - - if m is not None: - self.model = m - - try: - self.Ainv - except AttributeError: - self.Ainv = len(self.survey.frequencies) * [None] - - f = self.fieldsPair(self) - - for i_f, freq in enumerate(self.survey.frequencies): - A = self.getA(freq) - rhs = self.getRHS(freq) - Ainv = self.solver(A, **self.solver_opts) - u = Ainv * rhs - if not self.forward_only: - self.Ainv[i_f] = Ainv - - Srcs = self.survey.get_sources_by_frequency(freq) - f[Srcs, self._solutionType] = u - return f - - # @profile - def Jvec(self, m, v, f=None): - r"""Compute the sensitivity matrix times a vector. - - Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, - and the sensitivity matrix is defined as: - - .. math:: - \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - - this method computes and returns the matrix-vector product: - - .. math:: - \mathbf{J v} - - for a given vector :math:`v`. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - v : (n_param,) numpy.ndarray - The vector. - f : .frequency_domain.fields.FieldsFDEM, optional - Fields solved for all sources. - - Returns - ------- - (n_data,) numpy.ndarray - The sensitivity matrix times a vector. - """ - - if f is None: - f = self.fields(m) - - self.model = m - - Jv = Data(self.survey) - - for nf, freq in enumerate(self.survey.frequencies): - for src in self.survey.get_sources_by_frequency(freq): - u_src = f[src, self._solutionType] - dA_dm_v = self.getADeriv(freq, u_src, v, adjoint=False) - dRHS_dm_v = self.getRHSDeriv(freq, src, v) - du_dm_v = self.Ainv[nf] * (-dA_dm_v + dRHS_dm_v) - for rx in src.receiver_list: - Jv[src, rx] = rx.evalDeriv(src, self.mesh, f, du_dm_v=du_dm_v, v=v) - - return Jv.dobs - - def Jtvec(self, m, v, f=None): - r"""Compute the adjoint sensitivity matrix times a vector. - - Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, - and the sensitivity matrix is defined as: - - .. math:: - \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - - this method computes and returns the matrix-vector product: - - .. math:: - \mathbf{J^T v} - - for a given vector :math:`v`. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - v : (n_data,) numpy.ndarray - The vector. - f : .frequency_domain.fields.FieldsFDEM, optional - Fields solved for all sources. - - Returns - ------- - (n_param,) numpy.ndarray - The adjoint sensitivity matrix times a vector. - """ - - if f is None: - f = self.fields(m) - - self.model = m - - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) - - Jtv = np.zeros(m.size) - - for nf, freq in enumerate(self.survey.frequencies): - for src in self.survey.get_sources_by_frequency(freq): - u_src = f[src, self._solutionType] - df_duT_sum = 0 - df_dmT_sum = 0 - for rx in src.receiver_list: - df_duT, df_dmT = rx.evalDeriv( - src, self.mesh, f, v=v[src, rx], adjoint=True - ) - if not isinstance(df_duT, Zero): - df_duT_sum += df_duT - if not isinstance(df_dmT, Zero): - df_dmT_sum += df_dmT - - ATinvdf_duT = self.Ainv[nf] * df_duT_sum - - dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) - dRHS_dmT = self.getRHSDeriv(freq, src, ATinvdf_duT, adjoint=True) - du_dmT = -dA_dmT + dRHS_dmT - - df_dmT_sum += du_dmT - Jtv += np.real(df_dmT_sum) - - return mkvc(Jtv) - - def getJ(self, m, f=None): - r"""Generate the full sensitivity matrix. - - This method generates and stores the full sensitivity matrix for the - model provided. I.e.: - - .. math:: - \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - - where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - f : .static.resistivity.fields.FieldsDC, optional - Fields solved for all sources. - - Returns - ------- - (n_data, n_param) numpy.ndarray - The full sensitivity matrix. - """ - self.model = m - - if getattr(self, "_Jmatrix", None) is None: - if f is None: - f = self.fields(m) - - Ainv = self.Ainv - m_size = self.model.size - - Jmatrix = np.zeros((self.survey.nD, m_size)) - - data = Data(self.survey) - - for A_i, freq in zip(Ainv, self.survey.frequencies): - for src in self.survey.get_sources_by_frequency(freq): - u_src = f[src, self._solutionType] - - for rx in src.receiver_list: - v = np.eye(rx.nD, dtype=float) - - df_duT, df_dmT = rx.evalDeriv( - src, self.mesh, f, v=v, adjoint=True - ) - - df_duT = np.hstack([df_duT]) - ATinvdf_duT = A_i * df_duT - dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) - dRHS_dmT = self.getRHSDeriv( - freq, src, ATinvdf_duT, adjoint=True - ) - du_dmT = -dA_dmT - - if not isinstance(dRHS_dmT, Zero): - du_dmT += dRHS_dmT - if not isinstance(df_dmT[0], Zero): - du_dmT += np.hstack(df_dmT) - - block = np.array(du_dmT, dtype=complex).real.T - data_inds = data.index_dictionary[src][rx] - Jmatrix[data_inds] = block - - self._Jmatrix = Jmatrix - - return self._Jmatrix - - def getJtJdiag(self, m, W=None, f=None): - r"""Return the diagonal of :math:`\mathbf{J^T J}`. - - Where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters, - the sensitivity matrix :math:`\mathbf{J}` is defined as: - - .. math:: - \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - - This method returns the diagonals of :math:`\mathbf{J^T J}`. When the - *W* input argument is used to include a diagonal weighting matrix - :math:`\mathbf{W}`, this method returns the diagonal of - :math:`\mathbf{W^T J^T J W}`. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - W : (n_param, n_param) scipy.sparse.csr_matrix - A diagonal weighting matrix. - f : .frequency_domain.fields.FieldsFDEM, optional - Fields solved for all sources. - - Returns - ------- - (n_param,) numpy.ndarray - The diagonals. - """ - self.model = m - - if getattr(self, "_gtgdiag", None) is None: - J = self.getJ(m, f=f) - - if W is None: - W = np.ones(J.shape[0]) - else: - W = W.diagonal() ** 2 - - diag = np.einsum("i,ij,ij->j", W, J, J) - - self._gtgdiag = diag - - return self._gtgdiag - - # @profile - def getSourceTerm(self, freq): - r"""Returns the discrete source terms for the frequency provided. - - This method computes and returns the discrete magnetic and electric source - terms for all soundings at the frequency provided. The exact shape and - implementation of the source terms when solving for the fields at each frequency - is formulation dependent. - - For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric - (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections - of the docstrings for: - - * :class:`.frequency_domain.Simulation3DElectricField` - * :class:`.frequency_domain.Simulation3DMagneticField` - * :class:`.frequency_domain.Simulation3DCurrentDensity` - * :class:`.frequency_domain.Simulation3DMagneticFluxDensity` - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - s_m : numpy.ndarray - The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. - s_e : numpy.ndarray - The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. - """ - Srcs = self.survey.get_sources_by_frequency(freq) - n_fields = sum(src._fields_per_source for src in Srcs) - if self._formulation == "EB": - s_m = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") - s_e = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") - elif self._formulation == "HJ": - s_m = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") - s_e = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") - - i = 0 - for src in Srcs: - ii = i + src._fields_per_source - smi, sei = src.eval(self) - if not isinstance(smi, Zero) and smi.ndim == 1: - smi = smi[:, None] - if not isinstance(sei, Zero) and sei.ndim == 1: - sei = sei[:, None] - s_m[:, i:ii] = s_m[:, i:ii] + smi - s_e[:, i:ii] = s_e[:, i:ii] + sei - i = ii - return s_m, s_e - - @property - def deleteTheseOnModelUpdate(self): - """List of model-dependent attributes to clean upon model update. - - Some of the FDEM simulation's attributes are model-dependent. This property specifies - the model-dependent attributes that much be cleared when the model is updated. - - Returns - ------- - list of str - List of the model-dependent attributes to clean upon model update. - """ - toDelete = super().deleteTheseOnModelUpdate - return toDelete + ["_Jmatrix", "_gtgdiag"] - - -############################################################################### -# E-B Formulation # -############################################################################### - - -class Simulation3DElectricField(BaseFDEMSimulation): - r"""3D FDEM simulation in terms of the electric field. - - This simulation solves for the electric field at each frequency. - In this formulation, the electric fields are defined on mesh edges and the - magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. - See the *Notes* section for a comprehensive description of the formulation. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh. - survey : .frequency_domain.survey.Survey - The frequency-domain EM survey. - forward_only : bool, optional - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - permittivity : (n_cells,) numpy.ndarray, optional - Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement - is ignored. Please note that `permittivity` is not an invertible property, and that future - development will result in the deprecation of this propery. - storeJ : bool, optional - Whether to compute and store the sensitivity matrix. - - Notes - ----- - Here, we start with the Maxwell's equations in the frequency-domain where a - :math:`+i\omega t` Fourier convention is used: - - .. math:: - \begin{align} - &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ - &\nabla \times \vec{H} - \vec{J} = \vec{S}_e - \end{align} - - where :math:`\vec{S}_e` is an electric source term that defines a source current density, - and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. - We define the constitutive relations for the electrical conductivity :math:`\sigma` - and magnetic permeability :math:`\mu` as: - - .. math:: - \vec{J} &= \sigma \vec{E} \\ - \vec{H} &= \mu^{-1} \vec{B} - - We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. - Through vector calculus identities and the divergence theorem, we obtain: - - .. math:: - & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv - + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv - = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ - & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv - - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da - - \int_\Omega \vec{u} \cdot \vec{J} \, dv - = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv - - Assuming natural boundary conditions, the surface integral is zero. - - The above expressions are discretized in space according to the finite volume method. - The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, - and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. - This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must - be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent - test functions discretized to edges and faces, respectively, we obtain the following - set of discrete inner-products: - - .. math:: - &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ - &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ - &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e \sigma} e} \\ - &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_e}` is the edge inner-product matrix - * :math:`\mathbf{M_f}` is the face inner-product matrix - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - By cancelling like-terms and combining the discrete expressions to solve for the electric field, we obtain: - - .. math:: - \mathbf{A \, e} = \mathbf{q} - - where - - * :math:`\mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}}` - * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m }` - - """ - - _solutionType = "eSolution" - _formulation = "EB" - fieldsPair = Fields3DElectricField - - def getA(self, freq): - r"""System matrix for the frequency provided. - - This method returns the system matrix for the frequency provided: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - - where - - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_edges, n_edges) sp.sparse.csr_matrix - The system matrix. - """ - - MfMui = self.MfMui - C = self.mesh.edge_curl - - if self.permittivity is None: - MeSigma = self.MeSigma - A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma - else: - Meyhat = self._get_edge_admittivity_property_matrix(freq) - A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * Meyhat - - return A - - def getADeriv_sigma(self, freq, u, v, adjoint=False): - r"""Conductivity derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - - where - - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - dMe_dsig_v = self.MeSigmaDeriv(u, v, adjoint) - return 1j * omega(freq) * dMe_dsig_v - - def getADeriv_mui(self, freq, u, v, adjoint=False): - r"""Inverse permeability derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - - where - - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - C = self.mesh.edge_curl - - if adjoint: - return self.MfMuiDeriv(C * u).T * (C * v) - - return C.T * (self.MfMuiDeriv(C * u) * v) - - def getADeriv(self, freq, u, v, adjoint=False): - r"""Derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - - where - - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties - :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - return ( - self.getADeriv_sigma(freq, u, v, adjoint) - + self.getADeriv_mui(freq, u, v, adjoint) - # + self.getADeriv_permittivity(freq, u, v, adjoint) - ) - - def getRHS(self, freq): - r"""Right-hand sides for the given frequency. - - This method returns the right-hand sides for the frequency provided. - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_edges, n_sources) numpy.ndarray - The right-hand sides. - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MfMui = self.MfMui - - return C.T * (MfMui * s_m) - 1j * omega(freq) * s_e - - def getRHSDeriv(self, freq, src, v, adjoint=False): - r"""Derivative of the right-hand side times a vector for a given source and frequency. - - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = -i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, - this method returns - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : int - The frequency in Hz. - src : .frequency_domain.sources.BaseFDEMSrc - The FDEM source object. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - C = self.mesh.edge_curl - MfMui = self.MfMui - s_m, s_e = self.getSourceTerm(freq) - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - MfMuiDeriv = self.MfMuiDeriv(s_m) - - if adjoint: - return ( - s_mDeriv(MfMui * (C * v)) - + MfMuiDeriv.T * (C * v) - - 1j * omega(freq) * s_eDeriv(v) - ) - return C.T * (MfMui * s_mDeriv(v) + MfMuiDeriv * v) - 1j * omega( - freq - ) * s_eDeriv(v) - - -class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): - r"""3D FDEM simulation in terms of the magnetic flux field. - - This simulation solves for the magnetic flux density at each frequency. - In this formulation, the electric fields are defined on mesh edges and the - magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. - See the *Notes* section for a comprehensive description of the formulation. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh. - survey : .frequency_domain.survey.Survey - The frequency-domain EM survey. - forward_only : bool, optional - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - permittivity : (n_cells,) numpy.ndarray, optional - Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement - is ignored. Please note that `permittivity` is not an invertible property, and that future - development will result in the deprecation of this propery. - storeJ : bool, optional - Whether to compute and store the sensitivity matrix. - - Notes - ----- - Here, we start with the Maxwell's equations in the frequency-domain where a - :math:`+i\omega t` Fourier convention is used: - - .. math:: - \begin{align} - &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ - &\nabla \times \vec{H} - \vec{J} = \vec{S}_e - \end{align} - - where :math:`\vec{S}_e` is an electric source term that defines a source current density, - and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. - We define the constitutive relations for the electrical conductivity :math:`\sigma` - and magnetic permeability :math:`\mu` as: - - .. math:: - \vec{J} &= \sigma \vec{E} \\ - \vec{H} &= \mu^{-1} \vec{B} - - We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. - Through vector calculus identities and the divergence theorem, we obtain: - - .. math:: - & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv - + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv - = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ - & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv - - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da - - \int_\Omega \vec{u} \cdot \vec{J} \, dv - = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv - - Assuming natural boundary conditions, the surface integral is zero. - - The above expressions are discretized in space according to the finite volume method. - The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, - and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. - This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must - be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent - test functions discretized to edges and faces, respectively, we obtain the following - set of discrete inner-products: - - .. math:: - &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ - &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ - &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ - &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_e}` is the edge inner-product matrix - * :math:`\mathbf{M_f}` is the face inner-product matrix - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - By cancelling like-terms and combining the discrete expressions to solve for the magnetic flux density, we obtain: - - .. math:: - \mathbf{A \, b} = \mathbf{q} - - where - - * :math:`\mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I}` - * :math:`\mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m}` - - """ - - _solutionType = "bSolution" - _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity - - def getA(self, freq): - r"""System matrix for the frequency provided. - - This method returns the system matrix for the frequency provided: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{I}` is the identity matrix - * :math:`\mathbf{C}` is the curl operator - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_faces, n_faces) sp.sparse.csr_matrix - The system matrix. - """ - - MfMui = self.MfMui - C = self.mesh.edge_curl - iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - - if self.permittivity is None: - MeSigmaI = self.MeSigmaI - A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega - else: - MeyhatI = self._get_edge_admittivity_property_matrix( - freq, invert_matrix=True - ) - A = C * (MeyhatI * (C.T.tocsr() * MfMui)) + iomega - - if self._makeASymmetric: - return MfMui.T.tocsr() * A - return A - - def getADeriv_sigma(self, freq, u, v, adjoint=False): - r"""Conductivity derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{I}` is the identity matrix - * :math:`\mathbf{C}` is the curl operator - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, - this method assumes the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - MfMui = self.MfMui - C = self.mesh.edge_curl - MeSigmaIDeriv = self.MeSigmaIDeriv - vec = C.T * (MfMui * u) - - if adjoint: - return MeSigmaIDeriv(vec, C.T * v, adjoint) - return C * MeSigmaIDeriv(vec, v, adjoint) - - # if adjoint: - # return MeSigmaIDeriv.T * (C.T * v) - # return C * (MeSigmaIDeriv * v) - - def getADeriv_mui(self, freq, u, v, adjoint=False): - r"""Inverse permeability derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{I}` is the identity matrix - * :math:`\mathbf{C}` is the curl operator - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, - this method assumes the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - MfMuiDeriv = self.MfMuiDeriv(u) - MeSigmaI = self.MeSigmaI - C = self.mesh.edge_curl - - if adjoint: - return MfMuiDeriv.T * (C * (MeSigmaI.T * (C.T * v))) - return C * (MeSigmaI * (C.T * (MfMuiDeriv * v))) - - def getADeriv(self, freq, u, v, adjoint=False): - r"""Derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{I}` is the identity matrix - * :math:`\mathbf{C}` is the curl operator - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, - this method assumes the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - if adjoint is True and self._makeASymmetric: - v = self.MfMui * v - - ADeriv = self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui( - freq, u, v, adjoint - ) - - if adjoint is False and self._makeASymmetric: - return self.MfMui.T * ADeriv - - return ADeriv - - def getRHS(self, freq): - r"""Right-hand sides for the given frequency. - - This method returns the right-hand sides for the frequency provided. - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_faces, n_sources) numpy.ndarray - The right-hand sides. - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - - if self.permittivity is None: - MeSigmaI = self.MeSigmaI - RHS = s_m + C * (MeSigmaI * s_e) - else: - MeyhatI = self._get_edge_admittivity_property_matrix( - freq, invert_matrix=True - ) - RHS = s_m + C * (MeyhatI * s_e) - - if self._makeASymmetric is True: - MfMui = self.MfMui - return MfMui.T * RHS - - return RHS - - def getRHSDeriv(self, freq, src, v, adjoint=False): - r"""Derivative of the right-hand side times a vector for a given source and frequency. - - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, - this method returns - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : int - The frequency in Hz. - src : .frequency_domain.sources.BaseFDEMSrc - The FDEM source object. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - C = self.mesh.edge_curl - s_m, s_e = src.eval(self) - MfMui = self.MfMui - - if self._makeASymmetric and adjoint: - v = self.MfMui * v - - # MeSigmaIDeriv = self.MeSigmaIDeriv(s_e) - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - - if not adjoint: - # RHSderiv = C * (MeSigmaIDeriv * v) - RHSderiv = C * self.MeSigmaIDeriv(s_e, v, adjoint) - SrcDeriv = s_mDeriv(v) + C * (self.MeSigmaI * s_eDeriv(v)) - elif adjoint: - # RHSderiv = MeSigmaIDeriv.T * (C.T * v) - RHSderiv = self.MeSigmaIDeriv(s_e, C.T * v, adjoint) - SrcDeriv = s_mDeriv(v) + s_eDeriv(self.MeSigmaI.T * (C.T * v)) - - if self._makeASymmetric is True and not adjoint: - return MfMui.T * (SrcDeriv + RHSderiv) - - return RHSderiv + SrcDeriv - - -############################################################################### -# H-J Formulation # -############################################################################### - - -class Simulation3DCurrentDensity(BaseFDEMSimulation): - r"""3D FDEM simulation in terms of the current density. - - This simulation solves for the current density at each frequency. - In this formulation, the magnetic fields are defined on mesh edges and the - current densities are defined on mesh faces; i.e. it is an HJ formulation. - See the *Notes* section for a comprehensive description of the formulation. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh. - survey : .frequency_domain.survey.Survey - The frequency-domain EM survey. - forward_only : bool, optional - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - permittivity : (n_cells,) numpy.ndarray, optional - Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement - is ignored. Please note that `permittivity` is not an invertible property, and that future - development will result in the deprecation of this propery. - storeJ : bool, optional - Whether to compute and store the sensitivity matrix. - - Notes - ----- - Here, we start with the Maxwell's equations in the frequency-domain where a - :math:`+i\omega t` Fourier convention is used: - - .. math:: - \begin{align} - &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ - &\nabla \times \vec{H} - \vec{J} = \vec{S}_e - \end{align} - - where :math:`\vec{S}_e` is an electric source term that defines a source current density, - and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. - For now, we neglect displacement current (the `permittivity` attribute is ``None``). - We define the constitutive relations for the electrical resistivity :math:`\rho` - and magnetic permeability :math:`\mu` as: - - .. math:: - \vec{E} &= \rho \vec{J} \\ - \vec{B} &= \mu \vec{H} - - We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. - Through vector calculus identities and the divergence theorem, we obtain: - - .. math:: - & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv - - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da - + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv - = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ - & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv - - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ - & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv - - Assuming natural boundary conditions, the surface integral is zero. - - The above expressions are discretized in space according to the finite volume method. - The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, - and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. - This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must - be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent - test functions discretized to edges and faces, respectively, we obtain the following - set of discrete inner-products: - - .. math:: - &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ - &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ - &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ - &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_e}` is the edge inner-product matrix - * :math:`\mathbf{M_f}` is the face inner-product matrix - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - By cancelling like-terms and combining the discrete expressions to solve for the current density, we obtain: - - .. math:: - \mathbf{A \, j} = \mathbf{q} - - where - - * :math:`\mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} + i\omega \mathbf{I}` - * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m}` - - """ - - _solutionType = "jSolution" - _formulation = "HJ" - fieldsPair = Fields3DCurrentDensity - - permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - - def __init__( - self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs - ): - super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) - self.permittivity = permittivity - - def getA(self, freq): - r"""System matrix for the frequency provided. - - This method returns the system matrix for the frequency provided. - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_faces, n_faces) sp.sparse.csr_matrix - The system matrix. - """ - - MeMuI = self.MeMuI - MfRho = self.MfRho - C = self.mesh.edge_curl - iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - - if self.permittivity is not None: - Mfyhati = self._get_face_admittivity_property_matrix( - freq, invert_model=True - ) - A = C * MeMuI * C.T.tocsr() * Mfyhati + iomega - else: - A = C * MeMuI * C.T.tocsr() * MfRho + iomega - - if self._makeASymmetric is True: - return MfRho.T.tocsr() * A - return A - - def getADeriv_rho(self, freq, u, v, adjoint=False): - r"""Resistivity derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\rho}` are the set of model parameters defining the resistivity, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - MeMuI = self.MeMuI - C = self.mesh.edge_curl - - if adjoint: - vec = C * (MeMuI.T * (C.T * v)) - return self.MfRhoDeriv(u, vec, adjoint) - return C * (MeMuI * (C.T * (self.MfRhoDeriv(u, v, adjoint)))) - - def getADeriv_mu(self, freq, u, v, adjoint=False): - r"""Permeability derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - C = self.mesh.edge_curl - MfRho = self.MfRho - - MeMuIDeriv = self.MeMuIDeriv(C.T * (MfRho * u)) - - if adjoint is True: - # if self._makeASymmetric: - # v = MfRho * v - return MeMuIDeriv.T * (C.T * v) - - Aderiv = C * (MeMuIDeriv * v) - # if self._makeASymmetric: - # Aderiv = MfRho.T * Aderiv - return Aderiv - - def getADeriv(self, freq, u, v, adjoint=False): - r"""Derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_faces,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - if adjoint and self._makeASymmetric: - v = self.MfRho * v - - ADeriv = self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( - freq, u, v, adjoint - ) - - if not adjoint and self._makeASymmetric: - return self.MfRho.T * ADeriv - - return ADeriv - - def getRHS(self, freq): - r"""Right-hand sides for the given frequency. - - This method returns the right-hand sides for the frequency provided. - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_faces, n_sources) numpy.ndarray - The right-hand sides. - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MeMuI = self.MeMuI - - RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e - if self._makeASymmetric is True: - MfRho = self.MfRho - return MfRho.T * RHS - - return RHS - - def getRHSDeriv(self, freq, src, v, adjoint=False): - r"""Derivative of the right-hand side times a vector for a given source and frequency. - - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, - this method returns - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : int - The frequency in Hz. - src : .frequency_domain.sources.BaseFDEMSrc - The FDEM source object. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - # RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e - # if self._makeASymmetric is True: - # MfRho = self.MfRho - # return MfRho.T*RHS - - C = self.mesh.edge_curl - MeMuI = self.MeMuI - MeMuIDeriv = self.MeMuIDeriv - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - s_m, _ = self.getSourceTerm(freq) - - if adjoint: - if self._makeASymmetric: - MfRho = self.MfRho - v = MfRho * v - CTv = C.T * v - return ( - s_mDeriv(MeMuI.T * CTv) - + MeMuIDeriv(s_m).T * CTv - - 1j * omega(freq) * s_eDeriv(v) - ) - - else: - RHSDeriv = C * (MeMuI * s_mDeriv(v) + MeMuIDeriv(s_m) * v) - 1j * omega( - freq - ) * s_eDeriv(v) - - if self._makeASymmetric: - MfRho = self.MfRho - return MfRho.T * RHSDeriv - return RHSDeriv - - -class Simulation3DMagneticField(BaseFDEMSimulation): - r"""3D FDEM simulation in terms of the magnetic field. - - This simulation solves for the magnetic field at each frequency. - In this formulation, the magnetic fields are defined on mesh edges and the - current densities are defined on mesh faces; i.e. it is an HJ formulation. - See the *Notes* section for a comprehensive description of the formulation. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh. - survey : .frequency_domain.survey.Survey - The frequency-domain EM survey. - forward_only : bool, optional - If ``True``, the factorization for the inverse of the system matrix at each - frequency is discarded after the fields are computed at that frequency. - If ``False``, the factorizations of the system matrices for all frequencies are stored. - permittivity : (n_cells,) numpy.ndarray, optional - Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement - is ignored. Please note that `permittivity` is not an invertible property, and that future - development will result in the deprecation of this propery. - storeJ : bool, optional - Whether to compute and store the sensitivity matrix. - - Notes - ----- - Here, we start with the Maxwell's equations in the frequency-domain where a - :math:`+i\omega t` Fourier convention is used: - - .. math:: - \begin{align} - &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ - &\nabla \times \vec{H} - \vec{J} = \vec{S}_e - \end{align} - - where :math:`\vec{S}_e` is an electric source term that defines a source current density, - and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. - For now, we neglect displacement current (the `permittivity` attribute is ``None``). - We define the constitutive relations for the electrical resistivity :math:`\rho` - and magnetic permeability :math:`\mu` as: - - .. math:: - \vec{E} &= \rho \vec{J} \\ - \vec{B} &= \mu \vec{H} - - We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. - Through vector calculus identities and the divergence theorem, we obtain: - - .. math:: - & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv - - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da - + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv - = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ - & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv - - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ - & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ - & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv - - Assuming natural boundary conditions, the surface integral is zero. - - The above expressions are discretized in space according to the finite volume method. - The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, - and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. - This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must - be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent - test functions discretized to edges and faces, respectively, we obtain the following - set of discrete inner-products: - - .. math:: - &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ - &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ - &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ - &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_e}` is the edge inner-product matrix - * :math:`\mathbf{M_f}` is the face inner-product matrix - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - By cancelling like-terms and combining the discrete expressions to solve for the magnetic field, we obtain: - - .. math:: - \mathbf{A \, h} = \mathbf{q} - - where - - * :math:`\mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}}` - * :math:`\mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m}` - - """ - - _solutionType = "hSolution" - _formulation = "HJ" - fieldsPair = Fields3DMagneticField - - def getA(self, freq): - r"""System matrix for the frequency provided. - - This method returns the system matrix for the frequency provided. - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_edges, n_edges) sp.sparse.csr_matrix - The system matrix. - """ - - MeMu = self.MeMu - C = self.mesh.edge_curl - - if self.permittivity is None: - MfRho = self.MfRho - return C.T.tocsr() * (MfRho * C) + 1j * omega(freq) * MeMu - else: - Mfyhati = self._get_face_admittivity_property_matrix( - freq, invert_model=True - ) - return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu - - def getADeriv_rho(self, freq, u, v, adjoint=False): - r"""Resistivity derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - C = self.mesh.edge_curl - if adjoint: - return self.MfRhoDeriv(C * u, C * v, adjoint) - return C.T * self.MfRhoDeriv(C * u, v, adjoint) - - def getADeriv_mu(self, freq, u, v, adjoint=False): - r"""Permeability derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - MeMuDeriv = self.MeMuDeriv(u) - - if adjoint is True: - return 1j * omega(freq) * (MeMuDeriv.T * v) - - return 1j * omega(freq) * (MeMuDeriv * v) - - def getADeriv(self, freq, u, v, adjoint=False): - r"""Derivative operation for the system matrix times a vector. - - The system matrix at each frequency is given by: - - .. math:: - \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - - where - - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, - :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete electric field solution, this method assumes - the discrete solution is fixed and returns - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : float - The frequency in Hz. - u : (n_edges,) numpy.ndarray - The solution for the fields for the current model at the specified frequency. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of system matrix times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - return self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( - freq, u, v, adjoint - ) - - def getRHS(self, freq): - r"""Right-hand sides for the given frequency. - - This method returns the right-hand sides for the frequency provided. - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Parameters - ---------- - freq : float - The frequency in Hz. - - Returns - ------- - (n_edges, n_sources) numpy.ndarray - The right-hand sides. - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - - if self.permittivity is None: - MfRho = self.MfRho - return s_m + C.T * (MfRho * s_e) - else: - Mfyhati = self._get_face_admittivity_property_matrix( - freq, invert_model=True - ) - return s_m + C.T * (Mfyhati * s_e) - - def getRHSDeriv(self, freq, src, v, adjoint=False): - r"""Derivative of the right-hand side times a vector for a given source and frequency. - - The right-hand side for each source is constructed according to: - - .. math:: - \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} - - where - - * :math:`\mathbf{C}` is the discrete curl operator - * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively - * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges - * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces - - See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` - for a full description of the formulation. - - Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, - this method returns - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} - - Or the adjoint operation - - .. math:: - \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} - - Parameters - ---------- - freq : int - The frequency in Hz. - src : .frequency_domain.sources.BaseFDEMSrc - The FDEM source object. - v : numpy.ndarray - The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. - adjoint : bool - Whether to perform the adjoint operation. - - Returns - ------- - numpy.ndarray - Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. - (n_param,) for the adjoint operation. - """ - - _, s_e = src.eval(self) - C = self.mesh.edge_curl - MfRho = self.MfRho - - # MfRhoDeriv = self.MfRhoDeriv(s_e) - # if not adjoint: - # RHSDeriv = C.T * (MfRhoDeriv * v) - # elif adjoint: - # RHSDeriv = MfRhoDeriv.T * (C * v) - if not adjoint: - RHSDeriv = C.T * (self.MfRhoDeriv(s_e, v, adjoint)) - elif adjoint: - RHSDeriv = self.MfRhoDeriv(s_e, C * v, adjoint) - - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - - return RHSDeriv + s_mDeriv(v) + C.T * (MfRho * s_eDeriv(v)) diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py deleted file mode 100644 index 1596b15ec2..0000000000 --- a/SimPEG/potential_fields/gravity/simulation.py +++ /dev/null @@ -1,514 +0,0 @@ -import warnings -import numpy as np -import scipy.constants as constants -from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz -from scipy.constants import G as NewtG - -from simpeg import props -from simpeg.utils import mkvc, sdiag - -from ...base import BasePDESimulation -from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation - -from ._numba_functions import ( - choclo, - _sensitivity_gravity_serial, - _sensitivity_gravity_parallel, - _forward_gravity_serial, - _forward_gravity_parallel, -) - -if choclo is not None: - from numba import jit - - @jit(nopython=True) - def kernel_uv(easting, northing, upward, radius): - """Kernel for Guv gradiometry component.""" - result = 0.5 * ( - choclo.prism.kernel_nn(easting, northing, upward, radius) - - choclo.prism.kernel_ee(easting, northing, upward, radius) - ) - return result - - CHOCLO_KERNELS = { - "gx": choclo.prism.kernel_e, - "gy": choclo.prism.kernel_n, - "gz": choclo.prism.kernel_u, - "gxx": choclo.prism.kernel_ee, - "gyy": choclo.prism.kernel_nn, - "gzz": choclo.prism.kernel_uu, - "gxy": choclo.prism.kernel_en, - "gxz": choclo.prism.kernel_eu, - "gyz": choclo.prism.kernel_nu, - "guv": kernel_uv, - } - - -def _get_conversion_factor(component): - """ - Return conversion factor for the given component - """ - if component in ("gx", "gy", "gz"): - conversion_factor = 1e8 - elif component in ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"): - conversion_factor = 1e12 - else: - raise ValueError(f"Invalid component '{component}'.") - return conversion_factor - - -class Simulation3DIntegral(BasePFSimulation): - """ - Gravity simulation in integral form. - - .. important:: - - Density model is assumed to be in g/cc. - - .. important:: - - Acceleration components ("gx", "gy", "gz") are returned in mgal - (:math:`10^{-5} m/s^2`). - - .. important:: - - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are - returned in Eotvos (:math:`10^{-9} s^{-2}`). - - Parameters - ---------- - mesh : discretize.TreeMesh or discretize.TensorMesh - Mesh use to run the gravity simulation. - survey : simpeg.potential_fields.gravity.Survey - Gravity survey with information of the receivers. - ind_active : (n_cells) numpy.ndarray, optional - Array that indicates which cells in ``mesh`` are active cells. - rho : numpy.ndarray, optional - Density array for the active cells in the mesh. - rhoMap : Mapping, optional - Model mapping. - sensitivity_dtype : numpy.dtype, optional - Data type that will be used to build the sensitivity matrix. - store_sensitivities : {"ram", "disk", "forward_only"} - Options for storing sensitivity matrix. There are 3 options - - - 'ram': sensitivities are stored in the computer's RAM - - 'disk': sensitivities are written to a directory - - 'forward_only': you intend only do perform a forward simulation and - sensitivities do not need to be stored - - sensitivity_path : str, optional - Path to store the sensitivity matrix if ``store_sensitivities`` is set - to ``"disk"``. Default to "./sensitivities". - engine : {"geoana", "choclo"}, optional - Choose which engine should be used to run the forward model. - numba_parallel : bool, optional - If True, the simulation will run in parallel. If False, it will - run in serial. If ``engine`` is not ``"choclo"`` this argument will be - ignored. - """ - - rho, rhoMap, rhoDeriv = props.Invertible("Density") - - def __init__( - self, - mesh, - rho=None, - rhoMap=None, - engine="geoana", - numba_parallel=True, - **kwargs, - ): - super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) - self.rho = rho - self.rhoMap = rhoMap - self._G = None - self._gtg_diagonal = None - self.modelMap = self.rhoMap - - # Warn if n_processes has been passed - if self.engine == "choclo" and "n_processes" in kwargs: - warnings.warn( - "The 'n_processes' will be ignored when selecting 'choclo' as the " - "engine in the gravity simulation.", - UserWarning, - stacklevel=1, - ) - self.n_processes = None - - # Define jit functions - if self.engine == "choclo": - if self.numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_parallel - self._forward_gravity = _forward_gravity_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_serial - self._forward_gravity = _forward_gravity_serial - - def fields(self, m): - """ - Forward model the gravity field of the mesh on the receivers in the survey - - Parameters - ---------- - m : (n_active_cells,) numpy.ndarray - Array with values for the model. - - Returns - ------- - (nD,) numpy.ndarray - Gravity fields generated by the given model on every receiver - location. - """ - self.model = m - if self.store_sensitivities == "forward_only": - # Compute the linear operation without forming the full dense G - if self.engine == "choclo": - fields = self._forward(self.rho) - else: - fields = mkvc(self.linear_operator()) - else: - fields = self.G @ (self.rho).astype(self.sensitivity_dtype, copy=False) - return np.asarray(fields) - - def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ - """ - self.model = m - - if W is None: - W = np.ones(self.survey.nD) - else: - W = W.diagonal() ** 2 - if getattr(self, "_gtg_diagonal", None) is None: - diag = np.zeros(self.G.shape[1]) - for i in range(len(W)): - diag += W[i] * (self.G[i] * self.G[i]) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.rhoDeriv).power(2).sum(axis=0)) - - def getJ(self, m, f=None): - """ - Sensitivity matrix - """ - return self.G.dot(self.rhoDeriv) - - def Jvec(self, m, v, f=None): - """ - Sensitivity times a vector - """ - dmu_dm_v = self.rhoDeriv @ v - return self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) - - def Jtvec(self, m, v, f=None): - """ - Sensitivity transposed times a vector - """ - Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) - return np.asarray(self.rhoDeriv.T @ Jtvec) - - @property - def G(self): - """ - Gravity forward operator - """ - if getattr(self, "_G", None) is None: - if self.engine == "choclo": - self._G = self._sensitivity_matrix() - else: - self._G = self.linear_operator() - return self._G - - @property - def gtg_diagonal(self): - """ - Diagonal of GtG - """ - if getattr(self, "_gtg_diagonal", None) is None: - return None - - return self._gtg_diagonal - - def evaluate_integral(self, receiver_location, components): - """ - Compute the forward linear relationship between the model and the physics at a point - and for all components of the survey. - - :param numpy.ndarray receiver_location: array with shape (n_receivers, 3) - Array of receiver locations as x, y, z columns. - :param list[str] components: List of gravity components chosen from: - 'gx', 'gy', 'gz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'guv' - :param float tolerance: Small constant to avoid singularity near nodes and edges. - :rtype numpy.ndarray: rows - :returns: ndarray with shape (n_components, n_cells) - Dense array mapping of the contribution of all active cells to data components:: - - rows = - g_1 = [g_1x g_1y g_1z] - g_2 = [g_2x g_2y g_2z] - ... - g_c = [g_cx g_cy g_cz] - - """ - dr = self._nodes - receiver_location - dx = dr[..., 0] - dy = dr[..., 1] - dz = dr[..., 2] - - node_evals = {} - if "gx" in components: - node_evals["gx"] = prism_fz(dy, dz, dx) - if "gy" in components: - node_evals["gy"] = prism_fz(dz, dx, dy) - if "gz" in components: - node_evals["gz"] = prism_fz(dx, dy, dz) - if "gxy" in components: - node_evals["gxy"] = prism_fzx(dy, dz, dx) - if "gxz" in components: - node_evals["gxz"] = prism_fzx(dx, dy, dz) - if "gyz" in components: - node_evals["gyz"] = prism_fzy(dx, dy, dz) - if "gxx" in components or "guv" in components: - node_evals["gxx"] = prism_fzz(dy, dz, dx) - if "gyy" in components or "guv" in components: - node_evals["gyy"] = prism_fzz(dz, dx, dy) - if "guv" in components: - node_evals["guv"] = (node_evals["gyy"] - node_evals["gxx"]) * 0.5 - # (NN - EE) / 2 - inside_adjust = False - if "gzz" in components: - node_evals["gzz"] = prism_fzz(dx, dy, dz) - # The below should be uncommented when we are able to give the index of a - # containing cell. - # if "gxx" not in node_evals or "gyy" not in node_evals: - # node_evals["gzz"] = prism_fzz(dx, dy, dz) - # else: - # inside_adjust = True - # # The below need to be adjusted for observation points within a cell. - # # because `gxx + gyy + gzz = -4 * pi * G * rho` - # # gzz = - gxx - gyy - 4 * np.pi * G * rho[in_cell] - # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] - - rows = {} - for component in set(components): - vals = node_evals[component] - if self._unique_inv is not None: - vals = vals[self._unique_inv] - cell_vals = ( - vals[0] - - vals[1] - - vals[2] - + vals[3] - - vals[4] - + vals[5] - + vals[6] - - vals[7] - ) - if inside_adjust and component == "gzz": - # should subtract 4 * pi to the cell containing the observation point - # just need a little logic to find the containing cell - # cell_vals[inside_cell] += 4 * np.pi - pass - if self.store_sensitivities == "forward_only": - rows[component] = cell_vals @ self.rho - else: - rows[component] = cell_vals - if len(component) == 3: - rows[component] *= constants.G * 1e12 # conversion for Eotvos - else: - rows[component] *= constants.G * 1e8 # conversion for mGal - - return np.stack( - [ - rows[component].astype(self.sensitivity_dtype, copy=False) - for component in components - ] - ) - - def _forward(self, densities): - """ - Forward model the fields of active cells in the mesh on receivers. - - Parameters - ---------- - densities : (n_active_cells) numpy.ndarray - Array containing the densities of the active cells in the mesh, in - g/cc. - - Returns - ------- - (nD,) numpy.ndarray - Always return a ``np.float64`` array. - """ - # Gather active nodes and the indices of the nodes for each active cell - active_nodes, active_cell_nodes = self._get_active_nodes() - # Allocate fields array - fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) - # Compute fields - index_offset = 0 - for components, receivers in self._get_components_and_receivers(): - n_components = len(components) - n_elements = n_components * receivers.shape[0] - for i, component in enumerate(components): - kernel_func = CHOCLO_KERNELS[component] - conversion_factor = _get_conversion_factor(component) - vector_slice = slice( - index_offset + i, index_offset + n_elements, n_components - ) - self._forward_gravity( - receivers, - active_nodes, - densities, - fields[vector_slice], - active_cell_nodes, - kernel_func, - constants.G * conversion_factor, - ) - index_offset += n_elements - return fields - - def _sensitivity_matrix(self): - """ - Compute the sensitivity matrix G - - Returns - ------- - (nD, n_active_cells) numpy.ndarray - """ - # Gather active nodes and the indices of the nodes for each active cell - active_nodes, active_cell_nodes = self._get_active_nodes() - # Allocate sensitivity matrix - shape = (self.survey.nD, self.nC) - if self.store_sensitivities == "disk": - sensitivity_matrix = np.memmap( - self.sensitivity_path, - shape=shape, - dtype=self.sensitivity_dtype, - order="C", # it's more efficient to write in row major - mode="w+", - ) - else: - sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) - # Start filling the sensitivity matrix - index_offset = 0 - for components, receivers in self._get_components_and_receivers(): - n_components = len(components) - n_rows = n_components * receivers.shape[0] - for i, component in enumerate(components): - kernel_func = CHOCLO_KERNELS[component] - conversion_factor = _get_conversion_factor(component) - matrix_slice = slice( - index_offset + i, index_offset + n_rows, n_components - ) - self._sensitivity_gravity( - receivers, - active_nodes, - sensitivity_matrix[matrix_slice, :], - active_cell_nodes, - kernel_func, - constants.G * conversion_factor, - ) - index_offset += n_rows - return sensitivity_matrix - - -class SimulationEquivalentSourceLayer( - BaseEquivalentSourceLayerSimulation, Simulation3DIntegral -): - """ - Equivalent source layer simulations - - Parameters - ---------- - mesh : discretize.BaseMesh - A 2D tensor or tree mesh defining discretization along the x and y directions - cell_z_top : numpy.ndarray or float - Define the elevations for the top face of all cells in the layer. If an array it should be the same size as - the active cell set. - cell_z_bottom : numpy.ndarray or float - Define the elevations for the bottom face of all cells in the layer. If an array it should be the same size as - the active cell set. - """ - - -class Simulation3DDifferential(BasePDESimulation): - r"""Finite volume simulation class for gravity. - - Notes - ----- - From Blakely (1996), the scalar potential :math:`\phi` outside the source region - is obtained by solving a Poisson's equation: - - .. math:: - \nabla^2 \phi = 4 \pi \gamma \rho - - where :math:`\gamma` is the gravitational constant and :math:`\rho` defines the - distribution of density within the source region. - - Applying the finite volumn method, we can solve the Poisson's equation on a - 3D voxel grid according to: - - .. math:: - \big [ \mathbf{D M_f D^T} \big ] \mathbf{u} = - \mathbf{M_c \, \rho} - """ - - rho, rhoMap, rhoDeriv = props.Invertible("Specific density (g/cc)") - - def __init__(self, mesh, rho=1.0, rhoMap=None, **kwargs): - super().__init__(mesh, **kwargs) - self.rho = rho - self.rhoMap = rhoMap - - self._Div = self.mesh.face_divergence - - def getRHS(self): - """Return right-hand side for the linear system""" - Mc = self.Mcc - rho = self.rho - return -Mc * rho - - def getA(self): - r""" - GetA creates and returns the A matrix for the Gravity nodal problem - - The A matrix has the form: - - .. math :: - - \mathbf{A} = \Div(\Mf Mui)^{-1}\Div^{T} - """ - # Constructs A with 0 dirichlet - if getattr(self, "_A", None) is None: - self._A = self._Div * self.Mf * self._Div.T.tocsr() - return self._A - - def fields(self, m=None): - r"""Compute fields - - **INCOMPLETE** - - Parameters - ---------- - m: (nP) np.ndarray - The model - - Returns - ------- - dict - The fields - """ - if m is not None: - self.model = m - - A = self.getA() - RHS = self.getRHS() - - Ainv = self.solver(A) - u = Ainv * RHS - - gField = 4.0 * np.pi * NewtG * 1e8 * self._Div * u - - return {"G": gField, "u": u} diff --git a/SimPEG/regularization/base.py b/SimPEG/regularization/base.py deleted file mode 100644 index 3165244228..0000000000 --- a/SimPEG/regularization/base.py +++ /dev/null @@ -1,2300 +0,0 @@ -from __future__ import annotations - -import numpy as np -from discretize.base import BaseMesh -from typing import TYPE_CHECKING -from .. import maps -from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction -from .. import utils -from .regularization_mesh import RegularizationMesh - -from simpeg.utils.code_utils import deprecate_property, validate_ndarray_with_shape - -if TYPE_CHECKING: - from scipy.sparse import csr_matrix - - -class BaseRegularization(BaseObjectiveFunction): - """Base regularization class. - - The ``BaseRegularization`` class defines properties and methods inherited by - SimPEG regularization classes, and is not directly used to construct inversions. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is set to :obj:`simpeg.maps.IdentityMap`. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. - Each value is a numpy.ndarray of shape(:py:property:`~.regularization.RegularizationMesh.n_cells`, ). - - """ - - _model = None - _parent = None - _W = None - - def __init__( - self, - mesh: RegularizationMesh | BaseMesh, - active_cells: np.ndarray | None = None, - mapping: maps.IdentityMap | None = None, - reference_model: np.ndarray | None = None, - units: str | None = None, - weights: dict | None = None, - **kwargs, - ): - if isinstance(mesh, BaseMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - raise TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - if weights is not None and not isinstance(weights, dict): - raise TypeError( - f"Invalid 'weights' of type '{type(weights)}'. " - "It must be a dictionary with strings as keys and arrays as values." - ) - - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - - super().__init__(nP=None, mapping=None, **kwargs) - self._regularization_mesh = mesh - self._weights = {} - if active_cells is not None: - self.active_cells = active_cells - self.mapping = mapping # Set mapping using the setter - self.reference_model = reference_model - self.units = units - if weights is not None: - self.set_weights(**weights) - - @property - def active_cells(self) -> np.ndarray: - """Active cells defined on the regularization mesh. - - A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` - that are active (i.e. updated) throughout the inversion. The values of inactive cells - remain equal to their starting model values. - - Returns - ------- - (n_cells, ) array of bool - - Notes - ----- - If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the - array as representing the indices of the active cells. When called however, the quantity - will have been internally converted to a boolean array. - """ - return self.regularization_mesh.active_cells - - @active_cells.setter - def active_cells(self, values: np.ndarray | None): - self.regularization_mesh.active_cells = values - - if values is not None: - volume_term = "volume" in self._weights - self._weights = {} - self._W = None - if volume_term: - self.set_weights(volume=self.regularization_mesh.vol) - - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - - @property - def model(self) -> np.ndarray: - """The model parameters. - - Returns - ------- - (n_param, ) numpy.ndarray - The model parameters. - """ - return self._model - - @model.setter - def model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - values = validate_ndarray_with_shape( - "model", values, shape=(self._nC_residual,), dtype=float - ) - - self._model = values - - @property - def mapping(self) -> maps.IdentityMap: - """Mapping from the inversion model parameters to the regularization mesh. - - Returns - ------- - simpeg.maps.BaseMap - The mapping from the inversion model parameters to the quantity defined on the - :py:class:`~.regularization.RegularizationMesh`. - """ - return self._mapping - - @mapping.setter - def mapping(self, mapping: maps.IdentityMap): - if mapping is None: - mapping = maps.IdentityMap() - if not isinstance(mapping, maps.IdentityMap): - raise TypeError( - f"'mapping' must be of type {maps.IdentityMap}. " - f"Value of type {type(mapping)} provided." - ) - self._mapping = mapping - - @property - def parent(self): - """ - The parent objective function - """ - return self._parent - - @parent.setter - def parent(self, parent): - combo_class = ComboObjectiveFunction - if not isinstance(parent, combo_class): - raise TypeError( - f"Invalid parent of type '{parent.__class__.__name__}'. " - f"Parent must be a {combo_class.__name__}." - ) - self._parent = parent - - @property - def units(self) -> str | None: - """Units for the model parameters. - - Some regularization classes behave differently depending on the units; e.g. 'radian'. - - Returns - ------- - str - Units for the model parameters. - """ - return self._units - - @units.setter - def units(self, units: str | None): - if units is not None and not isinstance(units, str): - raise TypeError( - f"'units' must be None or type str. " - f"Value of type {type(units)} provided." - ) - self._units = units - - @property - def _weights_shapes(self) -> tuple[int] | str: - """Acceptable lengths for the weights - - Returns - ------- - list of tuple - Each tuple represents accetable shapes for the weights - """ - if ( - getattr(self, "_regularization_mesh", None) is not None - and self.regularization_mesh.nC != "*" - ): - return (self.regularization_mesh.nC,) - - if getattr(self, "_mapping", None) is not None and self.mapping.shape != "*": - return (self.mapping.shape[0],) - - return ("*",) - - @property - def reference_model(self) -> np.ndarray: - """Reference model. - - Returns - ------- - None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - """ - return self._reference_model - - @reference_model.setter - def reference_model(self, values: np.ndarray | float): - if values is not None: - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - values = validate_ndarray_with_shape( - "reference_model", values, shape=(self._nC_residual,), dtype=float - ) - self._reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - - @property - def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh. - - Mesh on which the regularization is discretized. This is not the same as - the mesh on which the simulation is defined. See :class:`.regularization.RegularizationMesh` - - Returns - ------- - .regularization.RegularizationMesh - Mesh on which the regularization is discretized. - """ - return self._regularization_mesh - - regmesh = deprecate_property( - regularization_mesh, - "regmesh", - "regularization_mesh", - "0.19.0", - error=True, - ) - - @property - def cell_weights(self) -> np.ndarray: - """Deprecated property for 'volume' and user defined weights.""" - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - @cell_weights.setter - def cell_weights(self, value): - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - def get_weights(self, key) -> np.ndarray: - """Cell weights for a given key. - - Parameters - ---------- - key: str - Name of the weights requested. - - Returns - ------- - (n_cells, ) numpy.ndarray - Cell weights for a given key. - - Examples - -------- - >>> import discretize - >>> from simpeg.regularization import Smallness - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = Smallness(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - - """ - return self._weights[key] - - def set_weights(self, **weights): - """Adds (or updates) the specified weights to the regularization. - - Parameters - ---------- - **kwargs : key, numpy.ndarray - Each keyword argument is added to the weights used by the regularization. - They can be accessed with their keyword argument. - - Examples - -------- - >>> import discretize - >>> from simpeg.regularization import Smallness - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = Smallness(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - - """ - for key, values in weights.items(): - values = validate_ndarray_with_shape( - "weights", values, shape=self._weights_shapes, dtype=float - ) - self._weights[key] = values - self._W = None - - @property - def weights_keys(self) -> list[str]: - """ - Return the keys for the existing cell weights - """ - return list(self._weights.keys()) - - def remove_weights(self, key): - """Removes the weights for the key provided. - - Parameters - ---------- - key : str - The key for the weights being removed from the cell weights dictionary. - - Examples - -------- - >>> import discretize - >>> from simpeg.regularization import Smallness - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = Smallness(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - >>> reg.remove_weights('my_weight') - """ - try: - self._weights.pop(key) - except KeyError as error: - raise KeyError(f"{key} is not in the weights dictionary") from error - self._W = None - - @property - def W(self) -> csr_matrix: - r"""Weighting matrix. - - Returns the weighting matrix for the discrete regularization function. To see how the - weighting matrix is constructed, see the *Notes* section for the :class:`Smallness` - regularization class. - - Returns - ------- - scipy.sparse.csr_matrix - The weighting matrix applied in the regularization. - """ - if getattr(self, "_W", None) is None: - weights = np.prod(list(self._weights.values()), axis=0) - self._W = utils.sdiag(weights**0.5) - return self._W - - @property - def _nC_residual(self) -> int: - """ - Shape of the residual - """ - - nC = getattr(self.regularization_mesh, "nC", None) - mapping = getattr(self, "_mapping", None) - - if mapping is not None and mapping.shape[1] != "*": - return self.mapping.shape[1] - - if nC != "*" and nC is not None: - return self.regularization_mesh.nC - - return self._weights_shapes[0] - - def _delta_m(self, m) -> np.ndarray: - if self.reference_model is None: - return m - return ( - m - self.reference_model - ) # in case self.reference_model is Zero, returns type m - - @utils.timeIt - def __call__(self, m): - """Evaluate the regularization function for the model provided. - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the function is evaluated. - - Returns - ------- - float - The regularization function evaluated for the model provided. - """ - r = self.W * self.f_m(m) - return r.dot(r) - - def f_m(self, m) -> np.ndarray: - """Not implemented for ``BaseRegularization`` class.""" - raise AttributeError("Regularization class must have a 'f_m' implementation.") - - def f_m_deriv(self, m) -> csr_matrix: - """Not implemented for ``BaseRegularization`` class.""" - raise AttributeError( - "Regularization class must have a 'f_m_deriv' implementation." - ) - - @utils.timeIt - def deriv(self, m) -> np.ndarray: - r"""Gradient of the regularization function evaluated for the model provided. - - Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative with respect to the model parameters; i.e. - the gradient: - - .. math:: - \frac{\partial \phi}{\partial \mathbf{m}} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the gradient is evaluated. - - Returns - ------- - (n_param, ) numpy.ndarray - The Gradient of the regularization function evaluated for the model provided. - """ - r = self.W * self.f_m(m) - return 2 * self.f_m_deriv(m).T * (self.W.T * r) - - @utils.timeIt - def deriv2(self, m, v=None) -> csr_matrix: - r"""Hessian of the regularization function evaluated for the model provided. - - Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method returns the second-derivative (Hessian) with respect to the model parameters: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} - - or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model for which the Hessian is evaluated. - v : None, (n_param, ) numpy.ndarray (optional) - A vector. - - Returns - ------- - (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the Hessian of the regularization - function for the model provided is returned. If *v* is not ``None``, - the Hessian multiplied by the vector provided is returned. - """ - f_m_deriv = self.f_m_deriv(m) - if v is None: - return 2 * f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) - - return 2 * f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) - - -class Smallness(BaseRegularization): - r"""Smallness regularization for least-squares inversion. - - ``Smallness`` regularization is used to ensure that differences between the - model values in the recovered model and the reference model are small; - i.e. it preserves structures in the reference model. If a reference model is not - supplied, the starting model will be set as the reference model in the - corresponding objective function by default. Optionally, custom cell weights can be - included to control the degree of smallness being enforced throughout different - regions the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : .regularization.RegularizationMesh - Mesh on which the regularization is discretized. Not the mesh used to - define the simulation. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to - a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh` . - - Notes - ----- - We define the regularization function (objective function) for smallness as: - - .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv - - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` - is a user-defined weighting function. - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: - - .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 - - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. - This is equivalent to an objective function of the form: - - .. math:: - \phi (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where - - - :math:`\mathbf{m}^{(ref)}` is a reference model (set using `reference_model`), and - - :math:`\mathbf{W}` is the weighting matrix. - - **Custom weights and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the objective function is given by: - - .. math:: - \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v}` are the cell volumes. - The weighting matrix used to apply the weights for smallness regularization is given by: - - .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = Smallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2) - - The default weights that account for cell dimensions in the regularization are accessed via: - - >>> reg.get_weights('volume') - - """ - - _multiplier_pair = "alpha_s" - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - self.set_weights(volume=self.regularization_mesh.vol) - - def f_m(self, m) -> np.ndarray: - r"""Evaluate the regularization kernel function. - - For smallness regularization, the regularization kernel function is given by: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - - where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{m}^{(ref)}` - is a reference model. For a more detailed description, see the *Notes* section below. - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - numpy.ndarray - The regularization kernel function evaluated for the model provided. - - Notes - ----- - The objective function for smallness regularization is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 - - """ - return self.mapping * self._delta_m(m) - - def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the regularization kernel function. - - For ``Smallness`` regularization, the derivative of the regularization kernel function - with respect to the model is given by: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - - where :math:`\mathbf{I}` is the identity matrix. - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - scipy.sparse.csr_matrix - The derivative of the regularization kernel function. - - Notes - ----- - The objective function for smallness regularization is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 - - Thus, the derivative with respect to the model is: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - - where :math:`\mathbf{I}` is the identity matrix. - """ - return self.mapping.deriv(self._delta_m(m)) - - -class SmoothnessFirstOrder(BaseRegularization): - r"""First-order smoothness least-squares regularization. - - ``SmoothnessFirstOrder`` regularization is used to ensure that values in the recovered - model are smooth along a specified direction. When a reference model is included, - the regularization preserves gradients/interfaces within the reference model along - the direction specified (x, y or z). Optionally, custom cell weights can be used - to control the degree of smoothness being enforced throughout different regions - the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : discretize.base.BaseMesh mesh - The mesh on which the regularization is discretized. - orientation : {'x', 'y', 'z'} - The direction along which smoothness is enforced. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. To include the reference model in the regularization, the - `reference_model_in_smooth` property must be set to ``True``. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness regularization. - units : None, str - Units for the model parameters. Some regularization classes behave differently - depending on the units; e.g. 'radian'. - weights : None, dict - Custom weights for the least-squares function. Each ``key`` points to - a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. - A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are - averaged to the appropriate faces internally when weighting is applied. - A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified - by the `orientation` input argument. - - Notes - ----- - We define the regularization function (objective function) for first-order smoothness - along the x-direction as: - - .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv - - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: - - .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 - - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh - and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. - This is equivalent to an objective function of the form: - - .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W \, G_x m } \, \Big \|^2 - - where - - - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, and - - :math:`\mathbf{W}` is the weighting matrix. - - Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, - :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. - - **Reference model in smoothness:** - - Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the regularization. - In this case, the objective function becomes: - - .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W G_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. - - **Custom weights and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for - smoothness along the x-direction. Each set of weights were either defined directly on the - faces or have been averaged from cell centers. - - The weighting applied within the objective function is given by: - - .. math:: - \mathbf{\tilde{w}} = \mathbf{v_x} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v_x}` are cell volumes projected to x-faces. - The weighting matrix is given by: - - .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - - Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. - A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are - averaged to the appropriate faces internally when weighting is applied. - A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified - by the `orientation` input argument. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers - >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces - >>> reg = SmoothnessFirstOrder( - >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} - >>> ) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2) - - The default weights that account for cell dimensions in the regularization are accessed via: - - >>> reg.get_weights('volume') - - """ - - def __init__( - self, mesh, orientation="x", reference_model_in_smooth=False, **kwargs - ): - self.reference_model_in_smooth = reference_model_in_smooth - - if orientation not in ["x", "y", "z"]: - raise ValueError("Orientation must be 'x', 'y' or 'z'") - - if orientation == "y" and mesh.dim < 2: - raise ValueError( - "Mesh must have at least 2 dimensions to regularize along the " - "y-direction." - ) - elif orientation == "z" and mesh.dim < 3: - raise ValueError( - "Mesh must have at least 3 dimensions to regularize along the " - "z-direction" - ) - self._orientation = orientation - - super().__init__(mesh=mesh, **kwargs) - self.set_weights(volume=self.regularization_mesh.vol) - - @property - def _weights_shapes(self): - """Acceptable lengths for the weights. - - Returns - ------- - tuple - A tuple of each acceptable lengths for the weights - """ - n_active_f, n_active_c = getattr( - self.regularization_mesh, "aveCC2F{}".format(self.orientation) - ).shape - return [(n_active_f,), (n_active_c,)] - - @property - def cell_gradient(self): - """Partial cell gradient operator. - - Returns the partial gradient operator which takes the derivative along the - orientation where smoothness is being enforced. For smoothness along the - x-direction, the resulting operator would map from cell centers to x-faces. - - Returns - ------- - scipy.sparse.csr_matrix - Partial cell gradient operator defined on the - :py:class:`.regularization.RegularizationMesh`. - """ - if getattr(self, "_cell_gradient", None) is None: - self._cell_gradient = getattr( - self.regularization_mesh, "cell_gradient_{}".format(self.orientation) - ) - return self._cell_gradient - - @property - def reference_model_in_smooth(self) -> bool: - # Inherited from BaseRegularization class - return self._reference_model_in_smooth - - @reference_model_in_smooth.setter - def reference_model_in_smooth(self, value: bool): - if not isinstance(value, bool): - raise TypeError( - "'reference_model_in_smooth must be of type 'bool'. " - f"Value of type {type(value)} provided." - ) - self._reference_model_in_smooth = value - - def _delta_m(self, m): - if self.reference_model is None or not self.reference_model_in_smooth: - return m - return m - self.reference_model - - @property - def _multiplier_pair(self): - return f"alpha_{self.orientation}" - - def f_m(self, m): - r"""Evaluate the regularization kernel function. - - For first-order smoothness regularization in the x-direction, - the regularization kernel function is given by: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction - (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model parameters defined on the - mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). - Similarly for smoothness along y and z. - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - numpy.ndarray - The regularization kernel function. - - Notes - ----- - The objective function for first-order smoothness regularization along the x-direction - is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. - See the :class:`SmoothnessFirstOrder` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 - """ - dfm_dl = self.mapping * self._delta_m(m) - - if self.units is not None and self.units.lower() == "radian": - return ( - utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) - / self._cell_distances - ) - return self.cell_gradient @ dfm_dl - - def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the regularization kernel function. - - For first-order smoothness regularization in the x-direction, the derivative of the - regularization kernel function with respect to the model is given by: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} - - where :math:`\mathbf{G_x}` is the partial cell gradient operator along x - (i.e. the x-derivative). - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - scipy.sparse.csr_matrix - The derivative of the regularization kernel function. - - Notes - ----- - The objective function for first-order smoothness regularization along the x-direction - is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. - See the :class:`SmoothnessFirstOrder` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 - - The derivative with respect to the model is therefore: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} - """ - return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) - - @property - def W(self) -> csr_matrix: - r"""Weighting matrix. - - Returns the weighting matrix for the objective function. To see how the - weighting matrix is constructed, see the *Notes* section for the - :class:`SmoothnessFirstOrder` regularization class. - - Returns - ------- - scipy.sparse.csr_matrix - The weighting matrix applied in the objective function. - """ - if getattr(self, "_W", None) is None: - average_cell_2_face = getattr( - self.regularization_mesh, "aveCC2F{}".format(self.orientation) - ) - weights = 1.0 - for values in self._weights.values(): - if values.shape[0] == self.regularization_mesh.nC: - values = average_cell_2_face * values - weights *= values - self._W = utils.sdiag(weights**0.5) - return self._W - - @property - def _cell_distances(self): - """ - Distances between cell centers for the cell center difference. - """ - return getattr(self.regularization_mesh, f"cell_distances_{self.orientation}") - - @property - def orientation(self): - """Direction along which smoothness is enforced. - - Returns - ------- - {'x','y','z'} - The direction along which smoothness is enforced. - - """ - return self._orientation - - -class SmoothnessSecondOrder(SmoothnessFirstOrder): - r"""Second-order smoothness (flatness) least-squares regularization. - - ``SmoothnessSecondOrder`` regularization is used to ensure that values in the recovered - model have small second-order spatial derivatives. When a reference model is included, - the regularization preserves second-order smoothness within the reference model along - the direction specified (x, y or z). Optionally, custom cell weights can be used - to control the degree of smoothness being enforced throughout different regions - the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : discretize.base.BaseMesh mesh - The mesh on which the regularization is discretized. - orientation : {'x', 'y', 'z'} - The direction along which smoothness is enforced. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. To include the reference model in the regularization, the - `reference_model_in_smooth` property must be set to ``True``. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness regularization. - units : None, str - Units for the model parameters. Some regularization classes behave differently - depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to - a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh`. - - Notes - ----- - We define the regularization function (objective function) for second-order - smoothness along the x-direction as: - - .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv - - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: - - .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the - mesh and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. - This is equivalent to an objective function of the form: - - .. math:: - \phi (\mathbf{m}) = \big \| \mathbf{W \, L_x \, m } \, \big \|^2 - - where - - - :math:`\mathbf{L_x}` is a second-order derivative operator with respect to :math:`x`, and - - :math:`\mathbf{W}` is the weighting matrix. - - **Reference model in smoothness:** - - Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization function. - In this case, the objective function becomes: - - .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W L_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. - - **Custom weights and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the objective function - is given by: - - .. math:: - \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v}` are the cell volumes. - The weighting matrix used to apply the weights is given by: - - .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = SmoothnessSecondOrder(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - """ - - def f_m(self, m): - r"""Evaluate the regularization kernel function. - - For second-order smoothness regularization in the x-direction, - the regularization kernel function is given by: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - where where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` - is the discrete second order x-derivative operator. - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - numpy.ndarray - The regularization kernel function. - - Notes - ----- - The objective function for second-order smoothness regularization along the x-direction - is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. - See the :class:`SmoothnessSecondOrder` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 - """ - dfm_dl = self.mapping * self._delta_m(m) - - if self.units is not None and self.units.lower() == "radian": - return self.cell_gradient.T @ ( - utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) - / self.length_scales - ) - - dfm_dl2 = self.cell_gradient @ dfm_dl - - return self.cell_gradient.T @ dfm_dl2 - - def f_m_deriv(self, m) -> csr_matrix: - r"""Derivative of the regularization kernel function. - - For second-order smoothness regularization, the derivative of the - regularization kernel function with respect to the model is given by: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - - where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. - - Parameters - ---------- - m : numpy.ndarray - The model. - - Returns - ------- - scipy.sparse.csr_matrix - The derivative of the regularization kernel function. - - Notes - ----- - The objective function for second-order smoothness regularization along the x-direction - is given by: - - .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. - See the :class:`SmoothnessSecondOrder` class documentation for more detail. - - We define the regularization kernel function :math:`\mathbf{f_m}` as: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - such that - - .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 - - The derivative of the regularization kernel function with respect to the model is: - - .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - """ - return ( - self.cell_gradient.T - @ self.cell_gradient - @ self.mapping.deriv(self._delta_m(m)) - ) - - @property - def W(self) -> csr_matrix: - r"""Weighting matrix. - - Returns the weighting matrix for the objective function. To see how the - weighting matrix is constructed, see the *Notes* section for the - :class:`SmoothnessSecondOrder` regularization class. - - Returns - ------- - scipy.sparse.csr_matrix - The weighting matrix applied in the objective function. - """ - if getattr(self, "_W", None) is None: - weights = np.prod(list(self._weights.values()), axis=0) - self._W = utils.sdiag(weights**0.5) - - return self._W - - @property - def _multiplier_pair(self): - return f"alpha_{self.orientation}{self.orientation}" - - -############################################################################### -# # -# Base Combo Regularization # -# # -############################################################################### - - -class WeightedLeastSquares(ComboObjectiveFunction): - r"""Weighted least-squares regularization using smallness and smoothness. - - Apply regularization using a weighted sum of :class:`Smallness`, :class:`SmoothnessFirstOrder`, - and/or :class:`SmoothnessSecondOrder` (optional) least-squares regularization functions. - ``Smallness`` regularization is used to ensure that values in the recovered model, - or differences between the recovered model and a reference model, are not overly - large in magnitude. ``Smoothness`` regularization is used to ensure that values in the - recovered model are smooth along specified directions. When a reference model - is included in the smoothness regularization, the inversion preserves - gradients/interfaces within the reference model. Custom weights can also be supplied - to control the degree of smallness and smoothness being - enforced throughout different regions the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) - numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. - alpha_s : float, optional - Scaling constant for the smallness regularization term. - alpha_x, alpha_y, alpha_z : float or None, optional - Scaling constants for the first order smoothness along x, y and z, respectively. - If set to ``None``, the scaling constant is set automatically according to the - value of the `length_scale` parameter. - alpha_xx, alpha_yy, alpha_zz : 0, float - Scaling constants for the second order smoothness along x, y and z, respectively. - If set to ``None``, the scaling constant is set automatically according to the - value of the `length_scale` parameter. - length_scale_x, length_scale_y, length_scale_z : float, optional - First order smoothness length scales for the respective dimensions. - - Notes - ----- - Weighted least-squares regularization can be defined by a weighted sum of - :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` - regularization functions. This corresponds to a model objective function - :math:`\phi_m (m)` of the form: - - .. math:: - \phi_m (m) =& \alpha_s \int_\Omega \, w(r) - \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv \\ - &+ \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) - \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^2 \, dv \\ - &+ \sum_{j=x,y,z} \alpha_{jj} \int_\Omega \, w(r) - \bigg [ \frac{\partial^2 m}{\partial \xi_j^2} \bigg ]^2 \, dv - \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` - is a user-defined weighting function. :math:`\xi_j` is the unit direction along :math:`j`. - Parameters :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` for :math:`j=x,y,z` - are multiplier constants which weight the respective contributions of the smallness and - smoothness terms towards the regularization. - - For implementation within SimPEG, the regularization functions and their variables - must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose - discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate - as follows: - - .. math:: - \int_\Omega w(r) \big [ x(r) \big ]^2 \, dv \approx \sum_i \tilde{w}_i \, | x_i |^2 - - where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions - in the discretization and apply user-defined weighting. Using the above approximation, - the ``WeightedLeastSquares`` regularization can be expressed as a weighted sum of - objective functions of the form: - - .. math:: - \phi_m (\mathbf{m}) =& \alpha_s - \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ - &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 - \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - - where - - - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), - - :math:`\mathbf{m}^{(ref)}` is the reference model, - - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, - - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, - - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices. - - **Reference model in smoothness:** - - Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization. - In this case, the objective function becomes: - - .. math:: - \phi_m (\mathbf{m}) =& \alpha_s - \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ - &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) - - This functionality is used by setting the `reference_model_in_smooth` parameter - to ``True``. - - **Alphas and length scales:** - - The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness - terms in the model objective function. Each :math:`\alpha` parameter can be set directly as a - appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set - using the `alpha_x` property. Note that unless the parameters are set manually, second-order - smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` - and `alpha_zz` parameters are set to 0 by default. - - The relative contributions of smallness and smoothness terms on the recovered model can also - be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling - constants based on length scales. The model objective function has been formulated such that - smallness and smoothness terms contribute equally when the length scales are equal; i.e. when - properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` - property is set, the `alpha_x` and `alpha_xx` properties are set internally as: - - >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 - - and - - >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 - - Likewise for y and z. - - **Custom weights and weighting matrices:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom - cell weights that are applied to all terms in the model objective function. - The general form for the weights applied to smallness and second-order smoothness terms - is given by: - - .. math:: - \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - - and weights applied to first-order smoothness terms are given by: - - .. math:: - \mathbf{\tilde{w}} = \big ( \mathbf{P \, v} \big ) \odot \prod_j \mathbf{P \, w_j} - - :math:`\mathbf{v}` are the cell volumes, and :math:`\mathbf{P}` represents the - projection matrix from cell centers to the appropriate faces; - i.e. where discrete first-order derivatives live. - - Weights for each term are used to construct their respective weighting matrices - as follows: - - .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = WeightedLeastSquares(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - """ - - _model = None - - def __init__( - self, - mesh, - active_cells=None, - alpha_s=1.0, - alpha_x=None, - alpha_y=None, - alpha_z=None, - alpha_xx=0.0, - alpha_yy=0.0, - alpha_zz=0.0, - length_scale_x=None, - length_scale_y=None, - length_scale_z=None, - mapping=None, - reference_model=None, - reference_model_in_smooth=False, - weights=None, - **kwargs, - ): - if isinstance(mesh, BaseMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - self._regularization_mesh = mesh - - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - - self.alpha_s = alpha_s - if alpha_x is not None: - if length_scale_x is not None: - raise ValueError( - "Attempted to set both alpha_x and length_scale_x at the same time. Please " - "use only one of them" - ) - self.alpha_x = alpha_x - else: - self.length_scale_x = length_scale_x - - if alpha_y is not None: - if length_scale_y is not None: - raise ValueError( - "Attempted to set both alpha_y and length_scale_y at the same time. Please " - "use only one of them" - ) - self.alpha_y = alpha_y - else: - self.length_scale_y = length_scale_y - - if alpha_z is not None: - if length_scale_z is not None: - raise ValueError( - "Attempted to set both alpha_z and length_scale_z at the same time. Please " - "use only one of them" - ) - self.alpha_z = alpha_z - else: - self.length_scale_z = length_scale_z - - # Check if weights is a dictionary, raise error if it's not - if weights is not None and not isinstance(weights, dict): - raise TypeError( - f"Invalid 'weights' of type '{type(weights)}'. " - "It must be a dictionary with strings as keys and arrays as values." - ) - - # do this to allow child classes to also pass a list of objfcts to this constructor - if "objfcts" not in kwargs: - objfcts = [ - Smallness(mesh=self.regularization_mesh), - SmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="x"), - SmoothnessSecondOrder(mesh=self.regularization_mesh, orientation="x"), - ] - - if mesh.dim > 1: - objfcts.extend( - [ - SmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="y" - ), - SmoothnessSecondOrder( - mesh=self.regularization_mesh, orientation="y" - ), - ] - ) - - if mesh.dim > 2: - objfcts.extend( - [ - SmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="z" - ), - SmoothnessSecondOrder( - mesh=self.regularization_mesh, orientation="z" - ), - ] - ) - else: - objfcts = kwargs.pop("objfcts") - - super().__init__(objfcts=objfcts, unpack_on_add=False, **kwargs) - - for fun in objfcts: - fun.parent = self - - if active_cells is not None: - self.active_cells = active_cells - - self.mapping = mapping - self.reference_model = reference_model - self.reference_model_in_smooth = reference_model_in_smooth - self.alpha_xx = alpha_xx - self.alpha_yy = alpha_yy - self.alpha_zz = alpha_zz - if weights is not None: - self.set_weights(**weights) - - def set_weights(self, **weights): - """Adds (or updates) the specified weights for all child regularization objects. - - Parameters - ---------- - **weights : key, numpy.ndarray - Each keyword argument is added to the weights used by all child regularization objects. - They can be accessed with their keyword argument. - - Examples - -------- - >>> import discretize - >>> from simpeg.regularization import WeightedLeastSquares - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = WeightedLeastSquares(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - - """ - for fct in self.objfcts: - fct.set_weights(**weights) - - def remove_weights(self, key): - """Removes specified weights from all child regularization objects. - - Parameters - ---------- - key : str - The name of the weights being removed from all child regularization objects. - - Examples - -------- - >>> import discretize - >>> from simpeg.regularization import WeightedLeastSquares - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = WeightedLeastSquares(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - >>> reg.remove_weights('my_weight') - """ - for fct in self.objfcts: - fct.remove_weights(key) - - @property - def cell_weights(self): - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - @cell_weights.setter - def cell_weights(self, value): - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - @property - def alpha_s(self): - """Multiplier constant for the smallness term. - - Returns - ------- - float - Multiplier constant for the smallness term. - """ - return self._alpha_s - - @alpha_s.setter - def alpha_s(self, value): - if value is None: - value = 1.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_s must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_s must be non-negative, not {value}") - self._alpha_s = value - - @property - def alpha_x(self): - """Multiplier constant for first-order smoothness along x. - - Returns - ------- - float - Multiplier constant for first-order smoothness along x. - """ - return self._alpha_x - - @alpha_x.setter - def alpha_x(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_x must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_x must be non-negative, not {value}") - self._alpha_x = value - - @property - def alpha_y(self): - """Multiplier constant for first-order smoothness along y. - - Returns - ------- - float - Multiplier constant for first-order smoothness along y. - """ - return self._alpha_y - - @alpha_y.setter - def alpha_y(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_y must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_y must be non-negative, not {value}") - self._alpha_y = value - - @property - def alpha_z(self): - """Multiplier constant for first-order smoothness along z. - - Returns - ------- - float - Multiplier constant for first-order smoothness along z. - """ - return self._alpha_z - - @alpha_z.setter - def alpha_z(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_z must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_z must be non-negative, not {value}") - self._alpha_z = value - - @property - def alpha_xx(self): - """Multiplier constant for second-order smoothness along x. - - Returns - ------- - float - Multiplier constant for second-order smoothness along x. - """ - return self._alpha_xx - - @alpha_xx.setter - def alpha_xx(self, value): - if value is None: - value = (self.length_scale_x * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_xx must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_xx must be non-negative, not {value}") - self._alpha_xx = value - - @property - def alpha_yy(self): - """Multiplier constant for second-order smoothness along y. - - Returns - ------- - float - Multiplier constant for second-order smoothness along y. - """ - return self._alpha_yy - - @alpha_yy.setter - def alpha_yy(self, value): - if value is None: - value = (self.length_scale_y * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_yy must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_yy must be non-negative, not {value}") - self._alpha_yy = value - - @property - def alpha_zz(self): - """Multiplier constant for second-order smoothness along z. - - Returns - ------- - float - Multiplier constant for second-order smoothness along z. - """ - return self._alpha_zz - - @alpha_zz.setter - def alpha_zz(self, value): - if value is None: - value = (self.length_scale_z * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_zz must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_zz must be non-negative, not {value}") - self._alpha_zz = value - - @property - def length_scale_x(self): - r"""Multiplier constant for smoothness along x relative to base scale length. - - Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), - and :math:`\alpha_x` defines the multiplier constant for first-order smoothness along x, - the length-scale is given by: - - .. math:: - L_x = \bigg ( \frac{\alpha_x}{\Delta h} \bigg )^{1/2} - - Returns - ------- - float - Multiplier constant for smoothness along x relative to base scale length. - """ - return np.sqrt(self.alpha_x) / self.regularization_mesh.base_length - - @length_scale_x.setter - def length_scale_x(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_x must be a real number, saw type{type(value)}" - ) - self.alpha_x = (value * self.regularization_mesh.base_length) ** 2 - - @property - def length_scale_y(self): - r"""Multiplier constant for smoothness along z relative to base scale length. - - Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), - and :math:`\alpha_y` defines the multiplier constant for first-order smoothness along y, - the length-scale is given by: - - .. math:: - L_y = \bigg ( \frac{\alpha_y}{\Delta h} \bigg )^{1/2} - - Returns - ------- - float - Multiplier constant for smoothness along z relative to base scale length. - """ - return np.sqrt(self.alpha_y) / self.regularization_mesh.base_length - - @length_scale_y.setter - def length_scale_y(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_y must be a real number, saw type{type(value)}" - ) - self.alpha_y = (value * self.regularization_mesh.base_length) ** 2 - - @property - def length_scale_z(self): - r"""Multiplier constant for smoothness along z relative to base scale length. - - Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), - and :math:`\alpha_z` defines the multiplier constant for first-order smoothness along z, - the length-scale is given by: - - .. math:: - L_z = \bigg ( \frac{\alpha_z}{\Delta h} \bigg )^{1/2} - - Returns - ------- - float - Multiplier constant for smoothness along z relative to base scale length. - """ - return np.sqrt(self.alpha_z) / self.regularization_mesh.base_length - - @length_scale_z.setter - def length_scale_z(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_z must be a real number, saw type{type(value)}" - ) - self.alpha_z = (value * self.regularization_mesh.base_length) ** 2 - - @property - def reference_model_in_smooth(self) -> bool: - """Whether to include the reference model in the smoothness objective functions. - - Returns - ------- - bool - Whether to include the reference model in the smoothness objective functions. - """ - return self._reference_model_in_smooth - - @reference_model_in_smooth.setter - def reference_model_in_smooth(self, value: bool): - if not isinstance(value, bool): - raise TypeError( - "'reference_model_in_smooth must be of type 'bool'. " - f"Value of type {type(value)} provided." - ) - self._reference_model_in_smooth = value - for fct in self.objfcts: - if getattr(fct, "reference_model_in_smooth", None) is not None: - fct.reference_model_in_smooth = value - - # Other properties and methods - @property - def nP(self): - """Number of model parameters. - - Returns - ------- - int - Number of model parameters. - """ - if getattr(self, "mapping", None) is not None and self.mapping.nP != "*": - return self.mapping.nP - elif ( - getattr(self, "_regularization_mesh", None) is not None - and self.regularization_mesh.nC != "*" - ): - return self.regularization_mesh.nC - else: - return "*" - - @property - def _nC_residual(self): - """ - Shape of the residual - """ - - nC = getattr(self.regularization_mesh, "nC", None) - mapping = getattr(self, "_mapping", None) - - if mapping is not None and mapping.shape[1] != "*": - return self.mapping.shape[1] - elif nC != "*" and nC is not None: - return self.regularization_mesh.nC - else: - return self.nP - - def _delta_m(self, m): - if self.reference_model is None: - return m - return m - self.reference_model - - @property - def multipliers(self): - r"""Multiplier constants for weighted sum of objective functions. - - For a model objective function :math:`\phi_m (\mathbf{m})` constructed using - a weighted sum of objective functions :math:`\phi_i (\mathbf{m})`, i.e.: - - .. math:: - \phi_m (\mathbf{m}) = \sum_i \alpha_i \, \phi_i (\mathbf{m}) - - the `multipliers` property returns the list of multiplier constants :math:`alpha_i` - in order. - - Returns - ------- - list of float - Multiplier constants for weighted sum of objective functions. - """ - return [getattr(self, objfct._multiplier_pair) for objfct in self.objfcts] - - @property - def active_cells(self) -> np.ndarray: - """Active cells defined on the regularization mesh. - - A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` - that are active (i.e. updated) throughout the inversion. The values of inactive cells - remain equal to their starting model values. - - Returns - ------- - (n_cells, ) array of bool - - Notes - ----- - If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the - array as representing the indices of the active cells. When called however, the quantity - will have been internally converted to a boolean array. - """ - return self.regularization_mesh.active_cells - - @active_cells.setter - def active_cells(self, values: np.ndarray): - self.regularization_mesh.active_cells = values - active_cells = self.regularization_mesh.active_cells - # notify the objective functions that the active_cells changed - for objfct in self.objfcts: - objfct.active_cells = active_cells - - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - - @property - def reference_model(self) -> np.ndarray: - """Reference model. - - Returns - ------- - None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - """ - return self._reference_model - - @reference_model.setter - def reference_model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - for fct in self.objfcts: - fct.reference_model = values - - self._reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - - @property - def model(self) -> np.ndarray: - """The model associated with regularization. - - Returns - ------- - (n_param, ) numpy.ndarray - The model parameters. - """ - return self._model - - @model.setter - def model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - for fct in self.objfcts: - fct.model = values - - self._model = values - - @property - def units(self) -> str: - """Units for the model parameters. - - Some regularization classes behave differently depending on the units; e.g. 'radian'. - - Returns - ------- - str - Units for the model parameters. - """ - return self._units - - @units.setter - def units(self, units: str | None): - if units is not None and not isinstance(units, str): - raise TypeError( - f"'units' must be None or type str. " - f"Value of type {type(units)} provided." - ) - for fct in self.objfcts: - fct.units = units - self._units = units - - @property - def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh. - - Mesh on which the regularization is discretized. This is not the same as - the mesh on which the simulation is defined. - - Returns - ------- - discretize.base.RegularizationMesh - Mesh on which the regularization is discretized. - """ - return self._regularization_mesh - - @property - def mapping(self) -> maps.IdentityMap: - """Mapping from the model to the regularization mesh. - - Returns - ------- - simpeg.maps.BaseMap - The mapping from the model parameters to the quantity defined on the - :py:class:`~simpeg.regularization.RegularizationMesh`. - """ - return self._mapping - - @mapping.setter - def mapping(self, mapping: maps.IdentityMap): - if mapping is None: - mapping = maps.IdentityMap(nP=self._nC_residual) - - if not isinstance(mapping, maps.IdentityMap): - raise TypeError( - f"'mapping' must be of type {maps.IdentityMap}. " - f"Value of type {type(mapping)} provided." - ) - self._mapping = mapping - - for fct in self.objfcts: - fct.mapping = mapping - - -############################################################################### -# # -# Base Coupling Regularization # -# # -############################################################################### -class BaseSimilarityMeasure(BaseRegularization): - """Base regularization class for joint inversion. - - The ``BaseSimilarityMeasure`` class defines properties and methods used - by regularization classes for joint inversion. It is not directly used to - constrain inversions. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh - Mesh on which the regularization is discretized. This is not necessarily the same as - the mesh on which the simulation is defined. - wire_map : simpeg.maps.WireMap - Wire map connecting physical properties defined on active cells of the - :class:`RegularizationMesh` to the entire model. - """ - - def __init__(self, mesh, wire_map, **kwargs): - super().__init__(mesh, **kwargs) - self.wire_map = wire_map - - @property - def wire_map(self): - """Mapping from model to physical properties defined on the regularization mesh. - - Returns - ------- - simpeg.maps.WireMap - Mapping from model to physical properties defined on the regularization mesh. - """ - return self._wire_map - - @wire_map.setter - def wire_map(self, wires): - try: - m1, m2 = wires.maps # Assume a map has been passed for each model. - except ValueError: - ValueError("Wire map must have two model mappings") - - if m1[1].shape[0] != m2[1].shape[0]: - raise ValueError( - f"All models must be the same size! Got {m1[1].shape[0]} and {m2[1].shape[0]}" - ) - self._wire_map = wires - - @property - def nP(self): - """Number of model parameters. - - Returns - ------- - int - Number of model parameters. - """ - return self.wire_map.nP - - def deriv(self, model): - """Not implemented for ``BaseSimilarityMeasure`` class.""" - raise NotImplementedError( - "The method deriv has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - def deriv2(self, model, v=None): - """Not implemented for ``BaseSimilarityMeasure`` class.""" - raise NotImplementedError( - "The method _deriv2 has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - @property - def _nC_residual(self): - """ - Shape of the residual - """ - return self.wire_map.nP - - def __call__(self, model): - """Not implemented for ``BaseSimilarityMeasure`` class.""" - raise NotImplementedError( - "The method __call__ has not been implemented for {}".format( - self.__class__.__name__ - ) - ) diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py deleted file mode 100644 index bf9353d652..0000000000 --- a/SimPEG/regularization/cross_gradient.py +++ /dev/null @@ -1,393 +0,0 @@ -import numpy as np -import scipy.sparse as sp - -from .base import BaseSimilarityMeasure -from ..utils import validate_type - - -############################################################################### -# # -# Cross-Gradient # -# # -############################################################################### - - -class CrossGradient(BaseSimilarityMeasure): - r"""Cross-gradient regularization for joint inversion. - - ``CrossGradient`` regularization is used to ensure the location and orientation of non-zero - gradients in the recovered model are consistent across two physical property distributions. - For joint inversion involving three or more physical properties, a separate instance of - ``CrossGradient`` must be created for each physical property pair and added to the total - regularization as a weighted sum. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - wire_map : simpeg.maps.Wires - Wire map connecting physical properties defined on active cells of the - :class:`RegularizationMesh`` to the entire model. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) - numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. - approx_hessian : bool - Whether to use the semi-positive definate approximation for the Hessian. - - Notes - ----- - Consider the case where the model is comprised of two physical properties - :math:`m_1` and :math:`m_2`. Here, we define the regularization - function (objective function) for cross-gradient as - (`Haber and Gazit, 2013 `__): - - .. math:: - \phi (m_1, m_2) = \int_\Omega \, w(r) \, - \Big | \nabla m_1 \, \times \, \nabla m_2 \, \Big |^2 \, dv - - where :math:`w(r)` is a user-defined weighting function. - Using the identity :math:`| \vec{a} \times \vec{b} |^2 = | \vec{a} |^2 | \vec{b} |^2 - (\vec{a} \cdot \vec{b})^2`, - the regularization function can be re-expressed as: - - .. math:: - \phi (m_1, m_2) = \int_\Omega \, w(r) \, \Big [ \, - \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 - - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is given by: - - .. math:: - \phi (m_1, m_2) \approx \sum_i \tilde{w}_i \, \bigg [ - \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 - - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] - - where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. - - In practice, we define the model :math:`\mathbf{m}` as a discrete - vector of the form: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - where :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}` are the discrete representations - of the respective physical properties on the mesh. The discrete regularization function - is therefore equivalent to an objective function of the form: - - .. math:: - \phi (\mathbf{m}) = - \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T - \Big [ \mathbf{W A} \big ( \mathbf{G \, m_2} \big )^2 \Big ] - - \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) - \odot \big ( \mathbf{G \, m_2} \big ) \Big ] \bigg \|^2 - - where exponents are computed elementwise, - - - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), - - :math:`\mathbf{A}` averages vectors from faces to cell centers, and - - :math:`\mathbf{W}` is the weighting matrix. - - **Custom weights and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. The weighting applied within the objective function is given by: - - .. math:: - \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v}` are the cell volumes. - The weighting matrix used to apply weights within the regularization is given by: - - .. math:: - \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = CrossGradient(mesh, wire_map, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - The default weights that account for cell dimensions in the regularization are accessed via: - - >>> reg.get_weights('volume') - - """ - - def __init__(self, mesh, wire_map, approx_hessian=True, **kwargs): - super().__init__(mesh, wire_map=wire_map, **kwargs) - self.approx_hessian = approx_hessian - - regmesh = self.regularization_mesh - - if regmesh.mesh.dim not in (2, 3): - raise ValueError("Cross-Gradient is only defined for 2D or 3D") - self._G = regmesh.cell_gradient - self._Av = sp.diags(np.sqrt(regmesh.vol)) * regmesh.average_face_to_cell - - @property - def approx_hessian(self): - """Whether to use the semi-positive definate approximation for the Hessian. - - Returns - ------- - bool - Whether to use the semi-positive definate approximation for the Hessian. - """ - return self._approx_hessian - - @approx_hessian.setter - def approx_hessian(self, value): - self._approx_hessian = validate_type("approx_hessian", value, bool) - - def _calculate_gradient(self, model, normalized=False, rtol=1e-6): - """ - Calculate the spatial gradients of the model using central difference. - - Concatenates gradient components into a single array. - [[x_grad1, y_grad1, z_grad1], - [x_grad2, y_grad2, z_grad2], - [x_grad3, y_grad3, z_grad3],...] - - :param numpy.ndarray model: model - - :rtype: numpy.ndarray - :return: gradient_vector: array where each row represents a model cell, - and each column represents a component of the gradient. - - """ - regmesh = self.regularization_mesh - Avs = [regmesh.aveFx2CC, regmesh.aveFy2CC] - if regmesh.dim == 3: - Avs.append(regmesh.aveFz2CC) - Av = sp.block_diag(Avs) - gradient = (Av @ (self._G @ model)).reshape((-1, regmesh.dim), order="F") - - if normalized: - norms = np.linalg.norm(gradient, axis=-1) - ind = norms <= norms.max() * rtol - norms[ind] = 1.0 - gradient /= norms[:, None] - gradient[ind] = 0.0 - # set gradient to 0 if amplitude of gradient is extremely small - - return gradient - - def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): - r"""Calculates the magnitudes of the cross-gradient vectors at cell centers. - - Computes and returns a discrete approximation to: - - .. math:: - \big | \, \nabla m_1 \, \times \, \nabla m_2 \, \big | - - at all cell centers where :math:`m_1` and :math:`m_2` define the continuous - spacial distribution of physical properties 1 and 2. - - Parameters - ---------- - model : numpy.ndarray - The input model, which will be automatically separated into the two - parameters internally. - normalized : bool, optional - Whether to normalize the cross-gradients. - rtol : float, optional - relative cuttoff for small gradients in the normalization. - - Returns - ------- - numpy.ndarray - Magnitudes of the cross-gradient vectors at cell centers. - """ - m1, m2 = self.wire_map * model - # Compute the gradients and concatenate components. - grad_m1 = self._calculate_gradient(m1, normalized=normalized, rtol=rtol) - grad_m2 = self._calculate_gradient(m2, normalized=normalized, rtol=rtol) - - # for each model cell, compute the cross product of the gradient vectors. - cross_prod = np.cross(grad_m1, grad_m2) - if self.regularization_mesh.dim == 3: - cross_prod = np.linalg.norm(cross_prod, axis=-1) - - return cross_prod - - def __call__(self, model): - """Evaluate the cross-gradient regularization function for the model provided. - - See the *Notes* section of the documentation for the :class:`CrossGradient` class - for a full description of the regularization function. - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model; a vector array containing all physical properties. - - Returns - ------- - float - The regularization function evaluated for the model provided. - """ - - m1, m2 = self.wire_map * model - Av = self._Av - G = self._G - g_m1 = G @ m1 - g_m2 = G @ m2 - return np.sum((Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2) - - def deriv(self, model): - r"""Gradient of the regularization function evaluated for the model provided. - - Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evaluates and returns the derivative with respect to the model parameters; - i.e. the gradient. For a model :math:`\mathbf{m}` consisting of two physical properties - such that: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - The gradient has the form: - - .. math:: - 2 \frac{\partial \phi}{\partial \mathbf{m}} = - \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ - \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} - - Parameters - ---------- - model : (n_param, ) numpy.ndarray - The model; a vector array containing all physical properties. - - Returns - ------- - (n_param, ) numpy.ndarray - Gradient of the regularization function evaluated for the model provided. - """ - m1, m2 = self.wire_map * model - - Av = self._Av - G = self._G - g_m1 = G @ m1 - g_m2 = G @ m2 - - return ( - 2 - * np.r_[ - (((Av @ g_m2**2) @ Av) * g_m1) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m2) @ G, - (((Av @ g_m1**2) @ Av) * g_m2) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m1) @ G, - ] - ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 - - def deriv2(self, model, v=None): - r"""Hessian of the regularization function evaluated for the model provided. - - Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: - For a model :math:`\mathbf{m}` consisting of two physical properties such that: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - The Hessian has the form: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = - \begin{bmatrix} - \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & - \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ - \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & - \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} - \end{bmatrix} - - When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian - times the vector: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} - - Parameters - ---------- - model : (n_param, ) numpy.ndarray - The model; a vector array containing all physical properties. - v : None, (n_param, ) numpy.ndarray (optional) - A numpy array to model the Hessian by. - - Returns - ------- - (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the Hessian - for the models provided is returned. If *v* is not ``None``, - the Hessian multiplied by the vector provided is returned. - """ - m1, m2 = self.wire_map * model - - Av = self._Av - G = self._G - - g_m1 = G @ m1 - g_m2 = G @ m2 - - d11_mid = Av.T @ (Av @ g_m2**2) - d12_mid = -(Av.T @ (Av @ (g_m1 * g_m2))) - d22_mid = Av.T @ (Av @ g_m1**2) - - if v is None: - D11_mid = sp.diags(d11_mid) - D12_mid = sp.diags(d12_mid) - D22_mid = sp.diags(d22_mid) - if not self.approx_hessian: - D11_mid = D11_mid - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m2) - D12_mid = ( - D12_mid - + 2 * sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m2) - - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m1) - ) - D22_mid = D22_mid - sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m1) - D11 = G.T @ D11_mid @ G - D12 = G.T @ D12_mid @ G - D22 = G.T @ D22_mid @ G - - return 2 * sp.bmat( - [[D11, D12], [D12.T, D22]], format="csr" - ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 - - else: - v1, v2 = self.wire_map * v - - Gv1 = G @ v1 - Gv2 = G @ v2 - p1 = G.T @ (d11_mid * Gv1 + d12_mid * Gv2) - p2 = G.T @ (d12_mid * Gv1 + d22_mid * Gv2) - if not self.approx_hessian: - p1 += G.T @ ( - -g_m2 * (Av.T @ (Av @ (g_m2 * Gv1))) # d11*v1 full addition - + 2 * g_m1 * (Av.T @ (Av @ (g_m2 * Gv2))) # d12*v2 full addition - - g_m2 * (Av.T @ (Av @ (g_m1 * Gv2))) # d12*v2 continued - ) - - p2 += G.T @ ( - -g_m1 * (Av.T @ (Av @ (g_m1 * Gv2))) # d22*v2 full addition - + 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) # d12.T*v1 full addition - - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued - ) - return ( - 2 * np.r_[p1, p2] - ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py deleted file mode 100644 index a917e7ecbd..0000000000 --- a/SimPEG/regularization/sparse.py +++ /dev/null @@ -1,1095 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from discretize.base import BaseMesh - -from .base import ( - BaseRegularization, - WeightedLeastSquares, - RegularizationMesh, - Smallness, - SmoothnessFirstOrder, -) -from .. import utils -from ..utils import ( - validate_ndarray_with_shape, - validate_float, - validate_type, - validate_string, -) - - -class BaseSparse(BaseRegularization): - """Base class for sparse-norm regularization. - - The ``BaseSparse`` class defines properties and methods inherited by sparse-norm - regularization classes. Sparse-norm regularization in SimPEG is implemented using - an iteratively re-weighted least squares (IRLS) approach. The ``BaseSparse`` class - however, is not directly used to define the regularization for the inverse problem. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model values used to constrain the inversion. If ``None``, the starting model - is set as the reference model. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) - numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. - norm : float - The norm used in the regularization function. Must be between within the interval [0, 2]. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. - If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. - - """ - - def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): - super().__init__(mesh=mesh, **kwargs) - self.norm = norm - self.irls_scaled = irls_scaled - self.irls_threshold = irls_threshold - - @property - def irls_scaled(self) -> bool: - """Scale IRLS weights. - - When ``True``, scaling is applied when computing IRLS weights. - The scaling acts to preserve the balance between the data misfit and the components of - the regularization based on the derivative of the l2-norm measure. And it assists the - convergence by ensuring the model does not deviate - aggressively from the global 2-norm solution during the first few IRLS iterations. - For a comprehensive description, see the documentation for :py:meth:`get_lp_weights` . - - Returns - ------- - bool - Whether to scale IRLS weights. - """ - return self._irls_scaled - - @irls_scaled.setter - def irls_scaled(self, value: bool): - self._irls_scaled = validate_type("irls_scaled", value, bool, cast=False) - - @property - def irls_threshold(self): - r"""Stability constant for computing IRLS weights. - - Returns - ------- - float - Stability constant for computing IRLS weights. - """ - return self._irls_threshold - - @irls_threshold.setter - def irls_threshold(self, value): - self._irls_threshold = validate_float( - "irls_threshold", value, min_val=0.0, inclusive_min=False - ) - - @property - def norm(self): - r"""Norm for the sparse regularization. - - Returns - ------- - None, float, (n_cells, ) numpy.ndarray - Norm for the sparse regularization. If ``None``, a 2-norm is used. - A float within the interval [0,2] represents a constant norm applied for all cells. - A ``numpy.ndarray`` object, where each entry is used to apply a different norm to each cell in the mesh. - """ - return self._norm - - @norm.setter - def norm(self, value: float | np.ndarray | None): - if value is None: - value = np.ones(self._weights_shapes[0]) * 2.0 - expected_shapes = self._weights_shapes - if isinstance(expected_shapes, list): - expected_shapes = expected_shapes[0] - value = validate_ndarray_with_shape( - "norm", value, shape=[expected_shapes, (1,)], dtype=float - ) - if value.shape == (1,): - value = np.full(expected_shapes[0], value) - - if np.any(value < 0) or np.any(value > 2): - raise ValueError( - "Value provided for 'norm' should be in the interval [0, 2]" - ) - self._norm = value - - def get_lp_weights(self, f_m): - r"""Compute and return iteratively re-weighted least-squares (IRLS) weights. - - For a regularization kernel function :math:`\mathbf{f_m}(\mathbf{m})` - evaluated at model :math:`\mathbf{m}`, compute and return the IRLS weights. - See :py:meth:`Smallness.f_m` and :py:meth:`SmoothnessFirstOrder.f_m` for examples of - least-squares regularization kernels. - - For :class:`SparseSmallness`, *f_m* is a (n_cells, ) ``numpy.ndarray``. - For :class:`SparseSmoothness`, *f_m* is a ``numpy.ndarray`` whose length corresponds - to the number of faces along a particular orientation; e.g. for smoothness along x, - the length is (n_faces_x, ). - - Parameters - ---------- - f_m : numpy.ndarray - The regularization kernel function evaluated at the current model. - - Notes - ----- - For a regularization kernel function :math:`\mathbf{f_m}` evaluated at model - :math:`\mathbf{m}`, the IRLS weights are computed via: - - .. math:: - \mathbf{w_r} = \boldsymbol{\lambda} \oslash - \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} - - where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small - constant added for stability of the algorithm (set using the `irls_threshold` property), - and :math:`\mathbf{p}` defines the `norm` at each cell. - - :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the components of - the regularization based on the derivative of the l2-norm measure. And it assists the - convergence by ensuring the model does not deviate - aggressively from the global 2-norm solution during the first few IRLS iterations. - - To apply elementwise scaling, let - - .. math:: - f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - - And define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: - - .. math:: - \tilde{f}_{\! i,max} = \begin{cases} - f_{max} \;\;\; for \; p_i \geq 1 \\ - \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\;\;\;\;\, for \; p_i < 1 - \end{cases} - - The elementwise scaling vector :math:`\boldsymbol{\lambda}` is: - - .. math:: - \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] - \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2} \bigg ]^{1 - \mathbf{p}/2} - """ - lp_scale = np.ones_like(f_m) - if self.irls_scaled: - # Scale on l2-norm gradient: f_m.max() - l2_max = np.ones_like(f_m) * np.abs(f_m).max() - # Compute theoretical maximum gradients for p < 1 - l2_max[self.norm < 1] = self.irls_threshold / np.sqrt( - 1.0 - self.norm[self.norm < 1] - ) - lp_values = l2_max / (l2_max**2.0 + self.irls_threshold**2.0) ** ( - 1.0 - self.norm / 2.0 - ) - lp_scale[lp_values != 0] = np.abs(f_m).max() / lp_values[lp_values != 0] - - return lp_scale / (f_m**2.0 + self.irls_threshold**2.0) ** ( - 1.0 - self.norm / 2.0 - ) - - -class SparseSmallness(BaseSparse, Smallness): - r"""Sparse smallness (compactness) regularization. - - ``SparseSmallness`` is used to recover models comprised of compact structures. - The level of compactness is controlled by the norm within the regularization - function; with more compact structures being recovered when a smaller norm is used. - Optionally, custom cell weights can be included to control the degree of compactness - being enforced throughout different regions the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : .regularization.RegularizationMesh - Mesh on which the regularization is discretized. Not the mesh used to - define the simulation. - norm : float, (n_cells, ) array_like - The norm defining sparseness in the regularization function. Use a ``float`` to define - the same norm for all mesh cells, or define an independent norm for each cell. All norm - values must be within the interval [0, 2]. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to - a (n_cells, ) numpy.ndarray that is defined on the - :py:class:`regularization.RegularizationMesh`. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. - If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. - - Notes - ----- - We define the regularization function (objective function) for sparse smallness (compactness) as: - - .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p(r)} \, dv - - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` - is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes - sparseness throughout the recovered model. More compact structures are recovered in regions - where :math:`p` is small. If the same level of sparseness is being imposed everywhere, - the exponent becomes a constant. - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: - - .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_i} - - where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply user-defined weighting. - :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). - - It is impractical to work with the general form directly, as its derivatives with respect - to the model are non-linear and discontinuous. Instead, the iteratively re-weighted - least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving - a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: - - .. math:: - \phi \big (\mathbf{m}^{(k)} \big ) - = \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} - \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 - - where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: - - .. math:: - r_i^{(k)} = \bigg [ \Big ( m_i^{(k-1)} - m_i^{(ref)} \Big )^2 + - \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} - - and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective - function for IRLS iteration :math:`k` can be expressed as follows: - - .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, - \mathbf{W}^{\! (k)} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - where - - - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, - - :math:`\mathbf{m}^{(ref)}` is a reference model (optional, set with `reference_model`), - - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. - - **IRLS weights, user-defined weighting and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights - for iteration :math:`k`. The net weighting applied within the objective function - is given by: - - .. math:: - \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{v} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v}` are the cell volumes. - For a description of how IRLS weights are updated at every iteration, see the documentation - for :py:meth:`update_weights`. - - The weighting matrix used to apply the weights is given by: - - .. math:: - \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - """ - - _multiplier_pair = "alpha_s" - - def update_weights(self, m): - r"""Update the IRLS weights for sparse smallness regularization. - - Parameters - ---------- - m : numpy.ndarray - The model used to update the IRLS weights. - - Notes - ----- - For the model :math:`\mathbf{m}` provided, the regularization kernel function - for sparse smallness is given by: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - - where :math:`\mathbf{m}^{(ref)}` is the reference model; see :py:meth:`Smallness.f_m` - for a more comprehensive definition. - - The IRLS weights are computed via: - - .. math:: - \mathbf{w_r} = \boldsymbol{\lambda} \oslash - \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} - - where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small - constant added for stability of the algorithm (set using the `irls_threshold` property), - and :math:`\mathbf{p}` defines the norm for each cell (defined using the `norm` property). - - :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the components of - the regularization based on the derivative of the l2-norm measure. And it assists the - convergence by ensuring the model does not deviate - aggressively from the global 2-norm solution during the first few IRLS iterations. - - To compute the scaling, let - - .. math:: - f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - - and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: - - .. math:: - \tilde{f}_{\! i,max} = \begin{cases} - f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ - \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 - \end{cases} - - The scaling quantity :math:`\boldsymbol{\lambda}` is: - - .. math:: - \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] - \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} - """ - f_m = self.f_m(m) - self.set_weights(irls=self.get_lp_weights(f_m)) - - -class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): - r"""Sparse smoothness (blockiness) regularization. - - ``SparseSmoothness`` is used to recover models comprised of blocky structures. - The level of blockiness is controlled by the choice in norm within the regularization - function; with more blocky structures being recovered when a smaller norm is used. - Optionally, custom cell weights can be included to control the degree of blockiness being - enforced throughout different regions the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : .regularization.RegularizationMesh - Mesh on which the regularization is discretized. Not the mesh used to - define the simulation. - orientation : {'x','y','z'} - The direction along which sparse smoothness is applied. - norm : float, array_like - The norm defining sparseness thoughout the regularization function. Must be within the - interval [0,2]. There are several options: - - - ``float``: constant sparse norm throughout the domain. - - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). - - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). - - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. To include the reference model in the regularization, the - `reference_model_in_smooth` property must be set to ``True``. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Custom weights for the least-squares function. Each ``key`` points to - a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. - A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are - averaged to the appropriate faces internally when weighting is applied. - A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified - by the `orientation` input argument. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. - If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. - gradient_type : {"total", "component"} - Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total - gradient or components of the gradient. - - Notes - ----- - The regularization function (objective function) for sparse smoothness (blockiness) - along the x-direction as: - - .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \Bigg | \, \frac{\partial m}{\partial x} \, \Bigg |^{p(r)} \, dv - - where :math:`m(r)` is the model, :math:`w(r)` - is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes - sparseness throughout the recovered model. Sharper boundaries are recovered in regions - where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, - the exponent becomes a constant. - - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discrete approximation for the regularization - function (objective function) is expressed in linear form as: - - .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \Bigg | \, \frac{\partial m_i}{\partial x} \, \Bigg |^{p_i} - - where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply user-defined weighting. - :math:`p_i \in \mathbf{p}` define the norm for each face (set using `norm`). - - It is impractical to work with the general form directly, as its derivatives with respect - to the model are non-linear and discontinuous. Instead, the iteratively re-weighted - least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving - a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: - - .. math:: - \phi \big (\mathbf{m}^{(k)} \big ) - = \sum_i - \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^{p_i} - \approx \sum_i \tilde{w}_i \, r_i^{(k)} - \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^2 - - where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: - - .. math:: - r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial m_i^{(k-1)}}{\partial x} \Bigg )^2 + - \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} - - and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective - function for IRLS iteration :math:`k` can be expressed as follows: - - .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, - \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 - - where - - - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, - - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), - - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. - - Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix - acts on variables living on x-faces. - - **Reference model in smoothness:** - - Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization. - In this case, the least-squares problem for IRLS iteration :math:`k` becomes: - - .. math:: - \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, - \mathbf{W}^{(k)} \mathbf{G_x} - \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. - - **IRLS weights, user-defined weighting and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of - custom weights defined on the faces specified by the `orientation` property; - i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined - directly on the faces or have been averaged from cell centers. And let - :math:`\mathbf{r_x}^{\!\! (k)}` represent the IRLS weights for iteration :math:`k`. - The net weighting applied within the objective function is given by: - - .. math:: - \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{v_x} \odot \prod_j \mathbf{w_j} - - where :math:`\mathbf{v_x}` are cell volumes projected to x-faces; i.e. where the - x-derivative lives. For a description of how IRLS weights are updated at every iteration, - see the documentation for :py:meth:`update_weights`. - - The weighting matrix used to apply the weights is given by: - - .. math:: - \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) - - Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. - A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are - averaged to the appropriate faces internally when weighting is applied. - A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified - by the `orientation` input argument. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers - >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces - >>> reg = SparseSmoothness( - >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} - >>> ) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - """ - - def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) - self.gradient_type = gradient_type - super().__init__(mesh=mesh, orientation=orientation, **kwargs) - - def update_weights(self, m): - r"""Update the IRLS weights for sparse smoothness regularization. - - Parameters - ---------- - m : numpy.ndarray - The model used to update the IRLS weights. - - Notes - ----- - Let us consider the IRLS weights for sparse smoothness along the x-direction. - When the class property `gradient_type`=`'components'`, IRLS weights are computed - using the regularization kernel function and we define: - - .. math:: - \mathbf{f_m} = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - - where :math:`\mathbf{m}` is the model provided, :math:`\mathbf{G_x}` is the partial cell - gradient operator along x (i.e. x-derivative), and :math:`\mathbf{m}^{(ref)}` is a - reference model (optional, activated using `reference_model_in_smooth`). - See :py:meth:`SmoothnessFirstOrder.f_m` for a more comprehensive definition of the - regularization kernel function. - - However, when the class property `gradient_type`=`'total'`, IRLS weights are computed - using the magnitude of the total gradient and we define: - - .. math:: - \mathbf{{f}_m} = \mathbf{A_{cx}} \sum_{j=x,y,z} \Big | \mathbf{A_j G_j} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big | - - where :math:`\mathbf{A_j}` for :math:`j=x,y,z` averages the partial gradients from their - respective faces to cell centers, and :math:`\mathbf{A_{cx}}` averages the sum of the - absolute values back to the appropriate faces. - - Once :math:`\mathbf{f_m}` is obtained, the IRLS weights are computed via: - - .. math:: - \mathbf{w_r} = \boldsymbol{\lambda} \oslash - \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} - - where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small - constant added for stability of the algorithm (set using the `irls_threshold` property), - and :math:`\mathbf{p}` defines the norm for each element (set using the `norm` property). - - :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights - (when the `irls_scaled` property is ``True``). - The scaling acts to preserve the balance between the data misfit and the components of - the regularization based on the derivative of the l2-norm measure. And it assists the - convergence by ensuring the model does not deviate - aggressively from the global 2-norm solution during the first few IRLS iterations. - - To apply the scaling, let - - .. math:: - f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty - - and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: - - .. math:: - \tilde{f}_{\! i,max} = \begin{cases} - f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ - \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 - \end{cases} - - The scaling vector :math:`\boldsymbol{\lambda}` is: - - .. math:: - \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] - \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} - """ - if self.gradient_type == "total" and self.parent is not None: - f_m = np.zeros(self.regularization_mesh.nC) - for obj in self.parent.objfcts: - if isinstance(obj, SparseSmoothness): - avg = getattr(self.regularization_mesh, f"aveF{obj.orientation}2CC") - f_m += np.abs(avg * obj.f_m(m)) - - f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m - - else: - f_m = self.f_m(m) - - self.set_weights(irls=self.get_lp_weights(f_m)) - - @property - def gradient_type(self) -> str: - """Gradient measure used to update IRLS weights for sparse smoothness. - - This property specifies whether the IRLS weights for sparse smoothness regularization - are updated using the total gradient (*"total"*) or using the partial gradient along - the smoothing orientation (*"components"*). To see how the IRLS weights are computed, - visit the documentation for :py:meth:`update_weights`. - - Returns - ------- - str in {"total", "components"} - Whether to re-weight using the total gradient or partial gradients along - smoothing orientations. - """ - return self._gradient_type - - @gradient_type.setter - def gradient_type(self, value: str): - self._gradient_type = validate_string( - "gradient_type", value, ["total", "components"] - ) - - gradientType = utils.code_utils.deprecate_property( - gradient_type, - "gradientType", - new_name="gradient_type", - removal_version="0.19.0", - error=True, - ) - - -class Sparse(WeightedLeastSquares): - r"""Sparse norm weighted least squares regularization. - - Apply regularization for recovering compact and/or blocky structures - using a weighted sum of :class:`SparseSmallness` and :class:`SparseSmoothness` - regularization functions. The level of compactness and blockiness is - controlled by the norms within the respective regularization functions; - with more sparse structures (compact and/or blocky) being recovered when smaller - norms are used. Optionally, custom cell weights can be applied to control - the degree of sparseness being enforced throughout different regions the model. - - See the *Notes* section below for a comprehensive description. - - Parameters - ---------- - mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh - Mesh on which the regularization is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - active_cells : None, (n_cells, ) numpy.ndarray of bool - Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` - cells that are active in the inversion. If ``None``, all cells are active. - mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. - reference_model : None, (n_param, ) numpy.ndarray - Reference model. If ``None``, the reference model in the inversion is set to - the starting model. - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - units : None, str - Units for the model parameters. Some regularization classes behave - differently depending on the units; e.g. 'radian'. - weights : None, dict - Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) - numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. - alpha_s : float, optional - Scaling constant for the smallness regularization term. - alpha_x, alpha_y, alpha_z : float or None, optional - Scaling constants for the first order smoothness along x, y and z, respectively. - If set to ``None``, the scaling constant is set automatically according to the - value of the `length_scale` parameter. - length_scale_x, length_scale_y, length_scale_z : float, optional - First order smoothness length scales for the respective dimensions. - gradient_type : {"total", "component"} - Gradient measure used in the IRLS re-weighting. Whether to re-weight using the - total gradient or components of the gradient. - norms : (dim+1, ) numpy.ndarray - The respective norms used for the sparse smallness, x-smoothness, (y-smoothness - and z-smoothness) regularization function. Must all be within the interval [0, 2]. - E.g. `np.r_[2, 1, 1, 1]` uses a 2-norm on the smallness term and a 1-norm on all - smoothness terms. - irls_scaled : bool - If ``True``, scale the IRLS weights to preserve magnitude of the regularization - function. If ``False``, do not scale. - irls_threshold : float - Constant added to IRLS weights to ensures stability in the algorithm. - - Notes - ----- - Sparse regularization can be defined by a weighted sum of - :class:`SparseSmallness` and :class:`SparseSmoothness` - regularization functions. This corresponds to a model objective function - :math:`\phi_m (m)` of the form: - - .. math:: - \phi_m (m) = \alpha_s \int_\Omega \, w(r) - \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p_s(r)} \, dv - + \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) - \Bigg | \, \frac{\partial m}{\partial \xi_j} \, \Bigg |^{p_j(r)} \, dv - - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, and :math:`w(r)` - is a user-defined weighting function applied to all terms. - :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. - Parameters :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` are multiplier constants - that weight the respective contributions of the smallness and smoothness terms in the - regularization. :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness - throughout the recovered model; where more compact structures are recovered in regions where - :math:`p_s(r)` is small. And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which - impose sparse smoothness throughout the recovered model along the specified direction; - where sharper boundaries are recovered in regions where these parameters are small. - - For implementation within SimPEG, regularization functions and their variables - must be discretized onto a `mesh`. For a regularization function whose kernel is given by - :math:`f(r)`, we approximate as follows: - - .. math:: - \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} - - where :math:`f_i \in \mathbf{f_m}` define the discrete regularization kernel function - on the mesh. For example, the regularization kernel function for smallness regularization - is: - - .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)} - - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply user-defined weighting. - :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). - - It is impractical to work with sparse norms directly, as their derivatives with respect - to the model are non-linear and discontinuous. Instead, the iteratively re-weighted - least-squares (IRLS) approach is used to approximate sparse norms by iteratively solving - a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: - - .. math:: - \sum_i \tilde{w}_i \, \Big | f_i^{(k)} \Big |^{p_i} - \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | f_i^{(k)} \Big |^2 - - where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: - - .. math:: - r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} - - and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). - For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the model - objective function for IRLS iteration :math:`k` can be expressed as a weighted sum of - objective functions of the form: - - .. math:: - \phi_m (\mathbf{m}) = \alpha_s - \Big \| \mathbf{W_s}^{\!\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - + \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j \, m} \Big \|^2 - - where - - - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), - - :math:`\mathbf{m}^{(ref)}` is the reference model, - - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, and - - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are the weighting matrices for iteration :math:`k`. - - The weighting matrices apply the IRLS weights, user-defined weighting, and account for cell - dimensions when the regularization functions are discretized. - - **IRLS weights, user-defined weighting and the weighting matrix:** - - Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell - weights that are applied to all objective functions in the model objective function. - For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness - term is given by: - - .. math:: - \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot - \mathbf{v} \odot \prod_j \mathbf{w_j} - - And for sparse smoothness along x (likewise for y and z) is given by: - - .. math:: - \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \big ( \mathbf{P_x \, v} \big ) - \odot \prod_j \mathbf{P_x \, w_j} - - The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` - for :math:`\ast = s,x,y,z`. :math:`\mathbf{v}` are the cell volumes. - Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` - project to the appropriate faces. - - Once the net weights for all objective functions are computed, - their weighting matrices can be constructed via: - - .. math:: - \mathbf{W}_\ast^{(k)} = \textrm{diag} \Big ( \, \sqrt{\mathbf{w_\ast}^{\!\! (k)} \, } \Big ) - - Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) - ``numpy.ndarray``. The weights can be set all at once during instantiation - with the `weights` keyword argument as follows: - - >>> reg = Sparse(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) - - or set after instantiation using the `set_weights` method: - - >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) - - **Reference model in smoothness:** - - Gradients/interfaces within a discrete reference model can be preserved by including the - reference model the smoothness regularization. In this case, - the objective function becomes: - - .. math:: - \phi_m (\mathbf{m}) = \alpha_s - \Big \| \mathbf{W_s}^{\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - + \sum_{j=x,y,z} \alpha_j \Big \| - \mathbf{W_j}^{\! (k)} \mathbf{G_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - - This functionality is used by setting the `reference_model_in_smooth` parameter - to ``True``. - - **Alphas and length scales:** - - The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness - terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an - appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set - using the `alpha_x` property. Note that unless the parameters are set manually, second-order - smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` - and `alpha_zz` parameters are set to 0 by default. - - The relative contributions of smallness and smoothness terms on the recovered model can also - be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling - constants based on length scales. The model objective function has been formulated such that - smallness and smoothness terms contribute equally when the length scales are equal; i.e. when - properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` - property is set, the `alpha_x` and `alpha_xx` properties are set internally as: - - >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 - - and - - >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 - - Likewise for y and z. - """ - - def __init__( - self, - mesh, - active_cells=None, - norms=None, - gradient_type="total", - irls_scaled=True, - irls_threshold=1e-8, - objfcts=None, - **kwargs, - ): - if not isinstance(mesh, RegularizationMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) - - self._regularization_mesh = mesh - if active_cells is not None: - self._regularization_mesh.active_cells = active_cells - - if objfcts is None: - objfcts = [ - SparseSmallness(mesh=self.regularization_mesh), - SparseSmoothness(mesh=self.regularization_mesh, orientation="x"), - ] - - if mesh.dim > 1: - objfcts.append( - SparseSmoothness(mesh=self.regularization_mesh, orientation="y") - ) - - if mesh.dim > 2: - objfcts.append( - SparseSmoothness(mesh=self.regularization_mesh, orientation="z") - ) - - super().__init__( - self.regularization_mesh, - objfcts=objfcts, - **kwargs, - ) - if norms is None: - norms = [1] * (mesh.dim + 1) - self.norms = norms - self.gradient_type = gradient_type - self.irls_scaled = irls_scaled - self.irls_threshold = irls_threshold - - @property - def gradient_type(self) -> str: - """Gradient measure used to update IRLS weights for sparse smoothness. - - This property specifies whether the IRLS weights for sparse smoothness regularization(s) - terms are updated using the total gradient (*"total"*) or using the partial gradients along - their smoothing orientations (*"components"*). To see how the IRLS weights are computed, - visit the documentation for :py:meth:`~SparseSmoothness.update_weights`. - - Returns - ------- - str in {"total", "components"} - Whether to re-weight using the total gradient or partial gradients along - smoothing orientations. - """ - return self._gradient_type - - @gradient_type.setter - def gradient_type(self, value: str): - for fct in self.objfcts: - if hasattr(fct, "gradient_type"): - fct.gradient_type = value - - self._gradient_type = value - - gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=True - ) - - @property - def norms(self): - """Norms for the child regularization classes. - - Norms for the smallness and all smoothness terms in the ``Sparse`` regularization. - - Returns - ------- - list of float or numpy.ndarray - Norms for the child regularization classes. - """ - return self._norms - - @norms.setter - def norms(self, values: list | np.ndarray | None): - if values is not None: - if len(values) != len(self.objfcts): - raise ValueError( - f"The number of values provided for 'norms', {len(values)}, does not " - f"match the number of regularization functions, {len(self.objfcts)}." - ) - else: - values = [None] * len(self.objfcts) - previous_norms = getattr(self, "_norms", [None] * len(self.objfcts)) - try: - for val, fct in zip(values, self.objfcts): - fct.norm = val - self._norms = values - except Exception as err: - # reset the norms if failed - for val, fct in zip(previous_norms, self.objfcts): - fct.norm = val - raise err - - @property - def irls_scaled(self) -> bool: - """Scale IRLS weights. - - Returns - ------- - bool - Scale the IRLS weights. - """ - return self._irls_scaled - - @irls_scaled.setter - def irls_scaled(self, value: bool): - value = validate_type("irls_scaled", value, bool, cast=False) - for fct in self.objfcts: - fct.irls_scaled = value - self._irls_scaled = value - - @property - def irls_threshold(self): - """IRLS stabilization constant. - - Constant added to the denominator of the IRLS weights for stability. - See documentation for the :class:`Sparse` class for a comprehensive description. - - Returns - ------- - float - IRLS stabilization constant. - """ - return self._irls_threshold - - @irls_threshold.setter - def irls_threshold(self, value): - value = validate_float( - "irls_threshold", value, min_val=0.0, inclusive_min=False - ) - for fct in self.objfcts: - fct.irls_threshold = value - self._irls_threshold = value - - def update_weights(self, model): - """Update IRLS weights for all child regularization objects. - - For an instance of the `Sparse` regularization class, this method re-computes and updates - the IRLS for all child regularization objects using the model provided. - To see how IRLS weights are recomputed for :class:`SparseSmallness` objects, visit the - documentation for :py:meth:`SparseSmallness.update_weights`. And for - :class:`SparseSmoothness` objects, visit the documentation for - :py:meth:`SparseSmoothness.update_weights`. - - Parameters - ---------- - model : (n_params, ) numpy.ndarray - The model used to recompute the IRLS weights. - """ - for fct in self.objfcts: - fct.update_weights(model) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py deleted file mode 100644 index e1091b1997..0000000000 --- a/SimPEG/simulation.py +++ /dev/null @@ -1,1053 +0,0 @@ -""" -Define simulation classes. -""" - -from __future__ import annotations # needed to use type operands in Python 3.8 -import os -import inspect -import numpy as np -import warnings - -from discretize.base import BaseMesh -from discretize import TensorMesh -from discretize.utils import unpack_widths, sdiag - -from . import props -from .typing import RandomSeed -from .data import SyntheticData, Data -from .survey import BaseSurvey -from .utils import ( - Counter, - timeIt, - count, - mkvc, - validate_ndarray_with_shape, - validate_float, - validate_type, - validate_string, - validate_integer, -) - -try: - from pymatsolver import Pardiso as DefaultSolver -except ImportError: - from .utils.solver_utils import SolverLU as DefaultSolver - -__all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] - - -############################################################################## -# # -# Simulation Base Classes # -# # -############################################################################## - - -class BaseSimulation(props.HasModel): - r"""Base class for all geophysical forward simulations in SimPEG. - - The ``BaseSimulation`` class defines properties and methods inherited by - practical simulation classes in SimPEG. - - .. important:: - This class is not meant to be instantiated. You should inherit from it to - create your own simulation class. - - Parameters - ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. - survey : simpeg.survey.BaseSurvey, optional - The survey for the simulation. - solver : None or pymatsolver.base.Base, optional - Numerical solver used to solve the forward problem. If ``None``, - an appropriate solver specific to the simulation class is set by default. - solver_opts : dict, optional - Solver-specific parameters. If ``None``, default parameters are used for - the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate - pairs of keyword arguments and parameter values for the solver. Please visit - `pymatsolver `__ to learn more - about solvers and their parameters. - sensitivity_path : str, optional - Path to directory where sensitivity file is stored. - counter : None or simpeg.utils.Counter - SimPEG ``Counter`` object to store iterations and run-times. - verbose : bool, optional - Verbose progress printout. - """ - - _REGISTRY = {} - - def __init__( - self, - mesh=None, - survey=None, - solver=None, - solver_opts=None, - sensitivity_path=None, - counter=None, - verbose=False, - **kwargs, - ): - self.mesh = mesh - self.survey = survey - if solver is None: - solver = DefaultSolver - self.solver = solver - if solver_opts is None: - solver_opts = {} - self.solver_opts = solver_opts - if sensitivity_path is None: - sensitivity_path = os.path.join(".", "sensitivity") - self.sensitivity_path = sensitivity_path - self.counter = counter - self.verbose = verbose - - super().__init__(**kwargs) - - @property - def mesh(self): - """Mesh for the simulation. - - For more on meshes, visit :py:class:`discretize.base.BaseMesh`. - - Returns - ------- - discretize.base.BaseMesh - Mesh on which the forward problem is discretized. - """ - return self._mesh - - @mesh.setter - def mesh(self, value): - if value is not None: - value = validate_type("mesh", value, BaseMesh, cast=False) - self._mesh = value - - @property - def survey(self): - """The survey for the simulation. - - Returns - ------- - simpeg.survey.BaseSurvey - The survey for the simulation. - """ - return self._survey - - @survey.setter - def survey(self, value): - if value is not None: - value = validate_type("survey", value, BaseSurvey, cast=False) - self._survey = value - - @property - def counter(self): - """SimPEG ``Counter`` object to store iterations and run-times. - - Returns - ------- - None or simpeg.utils.Counter - SimPEG ``Counter`` object to store iterations and run-times. - """ - return self._counter - - @counter.setter - def counter(self, value): - if value is not None: - value = validate_type("counter", value, Counter, cast=False) - self._counter = value - - @property - def sensitivity_path(self): - """Path to directory where sensitivity file is stored. - - Returns - ------- - str - Path to directory where sensitivity file is stored. - """ - return self._sensitivity_path - - @sensitivity_path.setter - def sensitivity_path(self, value): - self._sensitivity_path = validate_string("sensitivity_path", value) - - @property - def solver(self): - r"""Numerical solver used in the forward simulation. - - Many forward simulations in SimPEG require solutions to discrete linear - systems of the form: - - .. math:: - \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} - - where :math:`\mathbf{A}` is an invertible matrix that depends on the - model :math:`\mathbf{m}`. The numerical solver can be set using the - ``solver`` property. In SimPEG, the - `pymatsolver `__ package - is used to create solver objects. Parameters specific to each solver - can be set manually using the ``solver_opts`` property. - - Returns - ------- - pymatsolver.base.Base - Numerical solver used to solve the forward problem. - """ - return self._solver - - @solver.setter - def solver(self, cls): - if cls is not None: - if not inspect.isclass(cls): - raise TypeError(f"solver must be a class, not a {type(cls)}") - if not hasattr(cls, "__mul__"): - raise TypeError("solver must support the multiplication operator, `*`.") - self._solver = cls - - @property - def solver_opts(self): - """Solver-specific parameters. - - The parameters specific to the solver set with the ``solver`` property are set - upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. - This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments - and parameter values. Please visit `pymatsolver `__ - to learn more about solvers and their parameters. - - Returns - ------- - dict - keyword arguments and parameters passed to the solver. - """ - return self._solver_opts - - @solver_opts.setter - def solver_opts(self, value): - self._solver_opts = validate_type("solver_opts", value, dict, cast=False) - - @property - def verbose(self): - """Verbose progress printout. - - Returns - ------- - bool - Verbose progress printout status. - """ - return self._verbose - - @verbose.setter - def verbose(self, value): - self._verbose = validate_type("verbose", value, bool) - - def fields(self, m=None): - r"""Return the computed geophysical fields for the model provided. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - - Returns - ------- - simpeg.fields.Fields - Computed geophysical fields for the model provided. - - """ - raise NotImplementedError("fields has not been implemented for this ") - - def dpred(self, m=None, f=None): - r"""Predicted data for the model provided. - - Parameters - ---------- - m : (n_param,) numpy.ndarray - The model parameters. - f : simpeg.fields.Fields, optional - If provided, will be used to compute the predicted data - without recalculating the fields. - - Returns - ------- - (n_data, ) numpy.ndarray - The predicted data vector. - """ - if self.survey is None: - raise AttributeError( - "The survey has not yet been set and is required to compute " - "data. Please set the survey for the simulation: " - "simulation.survey = survey" - ) - - if f is None: - if m is None: - m = self.model - - f = self.fields(m) - - data = Data(self.survey) - for src in self.survey.source_list: - for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, f) - return mkvc(data) - - @timeIt - def Jvec(self, m, v, f=None): - r"""Compute the Jacobian times a vector for the model provided. - - The Jacobian defines the derivative of the predicted data vector with respect to the - model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters - :math:`\mathbf{m}`, the Jacobian is an (n_data, n_param) matrix whose elements - are given by: - - .. math:: - J_{ij} = \frac{\partial d_i}{\partial m_j} - - For a model `m` and vector `v`, the ``Jvec`` method computes the matrix-vector product - - .. math:: - \mathbf{u} = \mathbf{J \, v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - v : (n_param, ) numpy.ndarray - Vector we are multiplying. - f : simpeg.field.Fields, optional - If provided, fields will not need to be recomputed for the - current model to compute `Jvec`. - - Returns - ------- - (n_data, ) numpy.ndarray - The Jacobian times a vector for the model and vector provided. - """ - raise NotImplementedError("Jvec is not yet implemented.") - - @timeIt - def Jtvec(self, m, v, f=None): - r"""Compute the Jacobian transpose times a vector for the model provided. - - The Jacobian defines the derivative of the predicted data vector with respect to the - model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters - :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements - are given by: - - .. math:: - J_{ij} = \frac{\partial d_i}{\partial m_j} - - For a model `m` and vector `v`, the ``Jtvec`` method computes the matrix-vector product with the adjoint-sensitivity - - .. math:: - \mathbf{u} = \mathbf{J^T \, v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - v : (n_data, ) numpy.ndarray - Vector we are multiplying. - f : simpeg.field.Fields, optional - If provided, fields will not need to be recomputed for the - current model to compute `Jtvec`. - - Returns - ------- - (n_param, ) numpy.ndarray - The Jacobian transpose times a vector for the model and vector provided. - """ - raise NotImplementedError("Jtvec is not yet implemented.") - - @timeIt - def Jvec_approx(self, m, v, f=None): - r"""Approximation of the Jacobian times a vector for the model provided. - - The Jacobian defines the derivative of the predicted data vector with respect to the - model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters - :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements - are given by: - - .. math:: - J_{ij} = \frac{\partial d_i}{\partial m_j} - - For a model `m` and vector `v`, the ``Jvec_approx`` method **approximates** - the matrix-vector product: - - .. math:: - \mathbf{u} = \mathbf{J \, v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - v : (n_data, ) numpy.ndarray - Vector we are multiplying. - f : simpeg.field.Fields, optional - If provided, fields will not need to be recomputed for the - current model to compute `Jtvec`. - - Returns - ------- - (n_param, ) numpy.ndarray - Approximation of the Jacobian times a vector for the model provided. - """ - return self.Jvec(m, v, f) - - @timeIt - def Jtvec_approx(self, m, v, f=None): - r"""Approximation of the Jacobian transpose times a vector for the model provided. - - The Jacobian defines the derivative of the predicted data vector with respect to the - model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters - :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements - are given by: - - .. math:: - J_{ij} = \frac{\partial d_i}{\partial m_j} - - For a model `m` and vector `v`, the ``Jtvec_approx`` method **approximates** - the matrix-vector product: - - .. math:: - \mathbf{u} = \mathbf{J^T \, v} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - v : (n_data, ) numpy.ndarray - Vector we are multiplying. - f : simpeg.field.Fields, optional - If provided, fields will not need to be recomputed for the - current model to compute `Jtvec`. - - Returns - ------- - (n_param, ) numpy.ndarray - Approximation of the Jacobian transpose times a vector for the model provided. - """ - return self.Jtvec(m, v, f) - - @count - def residual(self, m, dobs, f=None): - r"""The data residual. - - This method computes and returns the data residual for the model provided. - Where :math:`\mathbf{d}_\text{obs}` are the observed data values, and :math:`\mathbf{d}_\text{pred}` - are the predicted data values for model parameters :math:`\mathbf{m}`, the data - residual is given by: - - .. math:: - \mathbf{r}(\mathbf{m}) = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - dobs : (n_data, ) numpy.ndarray - The observed data values. - f : simpeg.fields.Fields, optional - If provided, fields will not need to be recomputed when solving the forward problem. - - Returns - ------- - (n_data, ) numpy.ndarray - The data residual. - - """ - return mkvc(self.dpred(m, f=f) - dobs) - - def make_synthetic_data( - self, - m, - relative_error=0.05, - noise_floor=0.0, - f=None, - add_noise=False, - random_seed: RandomSeed | None = None, - **kwargs, - ): - r"""Make synthetic data for the model and Gaussian noise provided. - - This method generates and returns a :py:class:`simpeg.data.SyntheticData` object - for the model and standard deviation of Gaussian noise provided. - - Parameters - ---------- - m : (n_param, ) numpy.ndarray - The model parameters. - relative_error : float, numpy.ndarray - Assign relative uncertainties to the data using relative error; sometimes - referred to as percent uncertainties. For each datum, we assume the - standard deviation of Gaussian noise is the relative error times the - absolute value of the datum; i.e. :math:`C_\text{err} \times |d|`. - noise_floor : float, numpy.ndarray - Assign floor/absolute uncertainties to the data. For each datum, we assume - standard deviation of Gaussian noise is equal to `noise_floor`. - f : simpeg.fields.Fields, optional - If provided, fields will not need to be recomputed when solving the - forward problem to obtain noiseless data. - add_noise : bool - Whether to add gaussian noise to the synthetic data or not. - random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional - Random seed used for random sampling. It can either be an int or - a predefined Numpy random number generator (see - ``numpy.random.default_rng``). - - Returns - ------- - simpeg.data.SyntheticData - A SimPEG synthetic data object, which organizes both clean and noisy data. - """ - - std = kwargs.pop("std", None) - if std is not None: - raise TypeError( - "The std parameter has been removed. " "Please use relative_error." - ) - - if f is None: - f = self.fields(m) - - dclean = self.dpred(m, f=f) - - if add_noise is True: - random_num_generator = np.random.default_rng(seed=random_seed) - std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) - noise = random_num_generator.normal(loc=0, scale=std, size=dclean.shape) - dobs = dclean + noise - else: - dobs = dclean - - return SyntheticData( - survey=self.survey, - dobs=dobs, - dclean=dclean, - relative_error=relative_error, - noise_floor=noise_floor, - ) - - -class BaseTimeSimulation(BaseSimulation): - r"""Base class for time domain simulations. - - The ``BaseTimeSimulation`` defines properties and methods that are required - when the finite volume approach is used to solve time-dependent forward simulations. - Presently, SimPEG discretizes in time using the backward Euler approach. - And as such, the user must now define the step lengths for the forward simulation. - - Parameters - ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - t0 : float, optional - Initial time, in seconds, for the time-dependent forward simulation. - time_steps : (n_steps, ) numpy.ndarray, optional - The time step lengths, in seconds, for the time domain simulation. - This property can be also be set using a compact form; see *Notes*. - - Notes - ----- - There are two ways in which the user can set the ``time_steps`` property - for the forward simulation. The most basic approach is to use a ``(n_steps, )`` - :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. - I.e.: - - >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] - - We can define also define the step lengths in compact for when the same - step length is reused multiple times in succession. In this case, the - ``time_steps`` property is set using a ``list`` of ``tuple``. Each - ``tuple`` contains the step length and number of times that step is repeated. - The time stepping defined above can be set equivalently with: - - >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] - - When set, the :py:func:`discretize.utils.unpack_widths` utility is - used to convert the ``list`` of ``tuple`` to its (n_steps, ) :py:class:`numpy.ndarray` - representation. - """ - - def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): - self.t0 = t0 - self.time_steps = time_steps - super().__init__(mesh=mesh, **kwargs) - - @property - def time_steps(self): - """Time step lengths, in seconds, for the time domain simulation. - - There are two ways in which the user can set the ``time_steps`` property - for the forward simulation. The most basic approach is to use a ``(n_steps, )`` - :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. - I.e.: - - >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] - - We can define also define the step lengths in compact for when the same - step length is reused multiple times in succession. In this case, the - ``time_steps`` property is set using a ``list`` of ``tuple``. Each - ``tuple`` contains the step length and number of times that step is repeated. - The time stepping defined above can be set equivalently with: - - >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] - - When set, the :py:func:`discretize.utils.unpack_widths` utility is - used to convert the ``list`` of ``tuple`` to its ``(n_steps, )`` :py:class:`numpy.ndarray` - representation. - - Returns - ------- - (n_steps, ) numpy.ndarray - The time step lengths for the time domain simulation. - """ - return self._time_steps - - @time_steps.setter - def time_steps(self, value): - if value is not None: - if isinstance(value, list): - value = unpack_widths(value) - value = validate_ndarray_with_shape("time_steps", value, shape=("*",)) - self._time_steps = value - del self.time_mesh - - @property - def t0(self): - """Initial time, in seconds, for the time-dependent forward simulation. - - Returns - ------- - float - Initial time, in seconds, for the time-dependent forward simulation. - """ - return self._t0 - - @t0.setter - def t0(self, value): - self._t0 = validate_float("t0", value) - del self.time_mesh - - @property - def time_mesh(self): - r"""Time mesh for easy interpolation to observation times. - - The time mesh is constructed internally from the :py:attr:`t0` and - :py:attr:`time_steps` properties using the :py:class:`discretize.TensorMesh` class. - The ``time_mesh`` property allows for easy interpolation from fields computed at - discrete time-steps, to an arbitrary set of observation - times within the continuous interval (:math:`t_0 , t_\text{end}`). - - Returns - ------- - discretize.TensorMesh - The time mesh. - """ - if getattr(self, "_time_mesh", None) is None: - self._time_mesh = TensorMesh( - [ - self.time_steps, - ], - x0=[self.t0], - ) - return self._time_mesh - - @time_mesh.deleter - def time_mesh(self): - if hasattr(self, "_time_mesh"): - del self._time_mesh - - @property - def nT(self): - """Total number of time steps. - - Returns - ------- - int - Total number of time steps. - """ - return self.time_mesh.n_cells - - @property - def times(self): - """Evaluation times. - - Returns the discrete set of times at which the fields are computed for - the forward simulation. - - Returns - ------- - (nT, ) numpy.ndarray - The discrete set of times at which the fields are computed for - the forward simulation. - """ - return self.time_mesh.nodes_x - - def dpred(self, m=None, f=None): - # Docstring inherited from BaseSimulation. - if self.survey is None: - raise AttributeError( - "The survey has not yet been set and is required to compute " - "data. Please set the survey for the simulation: " - "simulation.survey = survey" - ) - - if f is None: - f = self.fields(m) - - data = Data(self.survey) - for src in self.survey.source_list: - for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, self.time_mesh, f) - return data.dobs - - -############################################################################## -# # -# Linear Simulation # -# # -############################################################################## - - -class LinearSimulation(BaseSimulation): - r"""Linear forward simulation class. - - The ``LinearSimulation`` class is used to define forward simulations of the form: - - .. math:: - \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) - - where :math:`\mathbf{m}` are the model parameters, :math:`\mathbf{f}` is a - mapping operator (optional) from the model space to a user-defined parameter space, - :math:`\mathbf{d}` is the predicted data vector, and :math:`\mathbf{G}` is an - ``(n_data, n_param)`` linear operator. - - The ``LinearSimulation`` class is generally used as a base class that is inherited by - other simulation classes within SimPEG. However, it can be used directly as a - simulation class if the :py:attr:`G` property is used to set the linear forward - operator directly. - - By default, we assume the mapping operator :math:`\mathbf{f}` is the identity map, - and that the forward simulation reduces to: - - .. math:: - \mathbf{d} = \mathbf{G \, m} - - Parameters - ---------- - mesh : discretize.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. - model_map : simpeg.maps.BaseMap - Mapping from the model parameters to vector that the linear operator acts on. - G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx - The linear operator. For a ``model_map`` that maps within the same vector space - (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. - If not, the dimension ``n_param`` of the linear operator will depend on the mapping. - """ - - linear_model, model_map, model_deriv = props.Invertible( - "The model for a linear problem" - ) - - def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwargs): - super().__init__(mesh=mesh, **kwargs) - self.linear_model = linear_model - self.model_map = model_map - self.solver = None - if G is not None: - self.G = G - - if self.survey is None: - # Give it an empty survey - self.survey = BaseSurvey([]) - if self.survey.nD == 0: - # try seting the number of data to G - if getattr(self, "G", None) is not None: - self.survey._vnD = np.r_[self.G.shape[0]] - - @property - def G(self): - """The linear operator. - - Returns - ------- - (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrix - The linear operator. For a :py:attr:`model_map` that maps within the same vector space - (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. - If not, the dimension ``n_param`` of the linear operator will depend on the mapping. - """ - if getattr(self, "_G", None) is not None: - return self._G - else: - warnings.warn("G has not been implemented for the simulation", stacklevel=2) - return None - - @G.setter - def G(self, G): - # Allows setting G in a LinearSimulation. - # TODO should be validated - self._G = G - - def fields(self, m): - # Docstring inherited from BaseSimulation. - self.model = m - return self.G.dot(self.linear_model) - - def dpred(self, m=None, f=None): - # Docstring inherited from BaseSimulation - if m is not None: - self.model = m - if f is not None: - return f - return self.fields(self.model) - - def getJ(self, m, f=None): - r"""Returns the full Jacobian. - - The general definition of the linear forward simulation is: - - .. math:: - \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) - - where :math:`\mathbf{f}` is a mapping operator (optional) from the model space - to a user-defined parameter space, and :math:`\mathbf{G}` is an (n_data, n_param) - linear operator. The ``getJ`` method forms and returns the full Jacobian: - - .. math:: - \mathbf{J}(\mathbf{m}) = \mathbf{G} \frac{\partial \mathbf{f}}{\partial \mathbf{m}} - - for the model :math:`\mathbf{m}` provided. When :math:`\mathbf{f}` is the identity map - (default), the Jacobian is no longer model-dependent and reduces to: - - .. math:: - \mathbf{J} = \mathbf{G} - - Parameters - ---------- - m : numpy.ndarray - The model vector. - f : None - Precomputed fields are not used to speed up the computation of the - Jacobian for linear problems. - - Returns - ------- - J : (n_data, n_param) numpy.ndarray - :math:`J = G\frac{\partial f}{\partial\mathbf{m}}`. - Where :math:`f` is :attr:`model_map`. - """ - self.model = m - # self.model_deriv is likely a sparse matrix - # and G is possibly dense, thus we need to do.. - return (self.model_deriv.T.dot(self.G.T)).T - - def Jvec(self, m, v, f=None): - # Docstring inherited from BaseSimulation - self.model = m - return self.G.dot(self.model_deriv * v) - - def Jtvec(self, m, v, f=None): - # Docstring inherited from BaseSimulation - self.model = m - return self.model_deriv.T * self.G.T.dot(v) - - -class ExponentialSinusoidSimulation(LinearSimulation): - r"""Simulation class for exponentially decaying sinusoidal kernel functions. - - This is the simulation class for the linear problem consisting of - exponentially decaying sinusoids. The entries of the linear operator - :math:`\mathbf{G}` are: - - .. math:: - - G_{ik} = \int_\Omega e^{p \, j_i \, x_k} \cos(\pi \, q \, j_i \, x_k) \, dx - - The model is defined on a 1D :py:class:`discretize.TensorMesh`, and :math:`x_k` - are the cell center locations. :math:`p \leq 0` defines the rate of exponential - decay of the kernel functions. :math:`q` defines the rate of oscillation of - the kernel functions. And :math:`j_i \in [j_0, ... , j_n]` controls the spread - of the kernel functions; the number of which is set using the ``n_kernels`` - property. - - .. tip:: - - For proper scaling, we advise defining the 1D tensor mesh to - discretize the interval [0, 1]. - - The kernel functions take the form: - - .. math:: - - \int_x e^{p j_k x} \cos(\pi q j_k x) \quad, j_k \in [j_0, ..., j_n] - - The model is defined at cell centers while the kernel functions are defined on nodes. - The trapezoid rule is used to evaluate the integral - - .. math:: - - d_j = \int g_j(x) m(x) dx - - to define our data. - - Parameters - ---------- - n_kernels : int - The number of kernel factors for the linear problem; i.e. the number of - :math:`j_i \in [j_0, ... , j_n]`. This sets the number of rows - in the linear forward operator. - p : float - Exponent specifying the decay (`p \leq 0`) or growth (`p \geq 0`) of the kernel. For decay, set :math:`p \leq 0`. - q : float - Rate of oscillation of the kernel. - j0 : float - Minimum value for the spread of the kernel factors. - jn : float - Maximum value for the spread of the kernel factors. - """ - - def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): - self.n_kernels = n_kernels - self.p = p - self.q = q - self.j0 = j0 - self.jn = jn - super(ExponentialSinusoidSimulation, self).__init__(**kwargs) - - @property - def n_kernels(self): - r"""The number of kernel factors for the linear problem. - - Where :math:`j_0` represents the minimum value for the spread of - kernel factors and :math:`j_n` represents the maximum, ``n_kernels`` - defines the number of kernel factors :math:`j_i \in [j_0, ... , j_n]`. - This ultimately sets the number of rows in the linear forward operator. - - Returns - ------- - int - The number of kernel factors for the linear problem. - """ - return self._n_kernels - - @n_kernels.setter - def n_kernels(self, value): - self._n_kernels = validate_integer("n_kernels", value, min_val=1) - - @property - def p(self): - """Rate of exponential decay of the kernel. - - Returns - ------- - float - Rate of exponential decay of the kernel. - """ - return self._p - - @p.setter - def p(self, value): - self._p = validate_float("p", value) - - @property - def q(self): - """Rate of oscillation of the kernel. - - Returns - ------- - float - Rate of oscillation of the kernel. - """ - return self._q - - @q.setter - def q(self, value): - self._q = validate_float("q", value) - - @property - def j0(self): - """Minimum value for the spread of the kernel factors. - - Returns - ------- - float - Minimum value for the spread of the kernel factors. - """ - return self._j0 - - @j0.setter - def j0(self, value): - self._j0 = validate_float("j0", value) - - @property - def jn(self): - """Maximum value for the spread of the kernel factors. - - Returns - ------- - float - Maximum value for the spread of the kernel factors. - """ - return self._jn - - @jn.setter - def jn(self, value): - self._jn = validate_float("jn", value) - - @property - def jk(self): - """The set of kernel factors controlling the spread of the kernel functions. - - Returns - ------- - (n_kernels, ) numpy.ndarray - The set of kernel factors controlling the spread of the kernel functions. - """ - if getattr(self, "_jk", None) is None: - self._jk = np.linspace(self.j0, self.jn, self.n_kernels) - return self._jk - - def g(self, k): - """Kernel functions evaluated for kernel factor :math:`j_k`. - - This method computes the row of the linear forward operator for - the kernel functions for kernel factor :math:`j_k`, given :math:`k` - - Parameters - ---------- - k : int - Kernel functions for kernel factor *k* - - Returns - ------- - (n_param, ) numpy.ndarray - Kernel functions evaluated for kernel factor *k*. - """ - return np.exp(self.p * self.jk[k] * self.mesh.nodes_x) * np.cos( - np.pi * self.q * self.jk[k] * self.mesh.nodes_x - ) - - @property - def G(self): - """The linear forward operator. - - Returns - ------- - (n_kernels, n_param) numpy.ndarray - The linear forward operator. - """ - if getattr(self, "_G", None) is None: - G_nodes = np.empty((self.mesh.n_nodes, self.n_kernels)) - - for i in range(self.n_kernels): - G_nodes[:, i] = self.g(i) - - self._G = (self.mesh.average_node_to_cell @ G_nodes).T @ sdiag( - self.mesh.cell_volumes - ) - return self._G From 88f95daa1d86e86939f60757fb01ef28985f7430 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 12 Jun 2024 10:37:27 -0700 Subject: [PATCH 449/455] Add files via upload --- simpeg/version.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 simpeg/version.py diff --git a/simpeg/version.py b/simpeg/version.py new file mode 100644 index 0000000000..7beb522965 --- /dev/null +++ b/simpeg/version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.21.2.dev55+g68a92075b.d20240520' +__version_tuple__ = version_tuple = (0, 21, 2, 'dev55', 'g68a92075b.d20240520') From da57e46650daf4de7045b1a6104ceaf47965d26d Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 12 Jun 2024 11:28:27 -0700 Subject: [PATCH 450/455] Fix bad import --- .../electromagnetics/natural_source/utils/plot_data_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py index aad9088c34..7eaee1e8be 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py @@ -1,4 +1,5 @@ -from matplotlib import pyplot as plt, colors, numpy as np +from matplotlib import pyplot as plt, colors +import numpy as np def plotIsoFreqNSimpedance( From 0ad02ac92475f80c1f9105bbb50d21fd056984a3 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 13 Jun 2024 08:40:23 -0700 Subject: [PATCH 451/455] Fix warning from numpy --- simpeg/dask/electromagnetics/frequency_domain/simulation.py | 4 ++-- simpeg/dask/electromagnetics/time_domain/simulation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/simpeg/dask/electromagnetics/frequency_domain/simulation.py b/simpeg/dask/electromagnetics/frequency_domain/simulation.py index 33d28d598b..eb8527ed8a 100644 --- a/simpeg/dask/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/dask/electromagnetics/frequency_domain/simulation.py @@ -118,11 +118,11 @@ def dask_dpred(self, m=None, f=None, compute_J=False): for rx in src.receiver_list: all_receivers.append((src, ind, rx)) - receiver_blocks = np.array_split(all_receivers, cpu_count()) + receiver_blocks = np.array_split(np.asarray(all_receivers), cpu_count()) rows = [] mesh = delayed(self.mesh) for block in receiver_blocks: - n_data = np.sum(rec.nD for _, _, rec in block) + n_data = np.sum([rec.nD for _, _, rec in block]) if n_data == 0: continue diff --git a/simpeg/dask/electromagnetics/time_domain/simulation.py b/simpeg/dask/electromagnetics/time_domain/simulation.py index f6300feab1..569d9f6ef1 100644 --- a/simpeg/dask/electromagnetics/time_domain/simulation.py +++ b/simpeg/dask/electromagnetics/time_domain/simulation.py @@ -220,7 +220,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): receiver_blocks = np.array_split(all_receivers, cpu_count()) for block in receiver_blocks: - n_data = np.sum(rec.nD for _, _, rec in block) + n_data = np.sum([rec.nD for _, _, rec in block]) if n_data == 0: continue @@ -563,7 +563,7 @@ def compute_J(self, f=None): ), dtype=np.float32, shape=( - np.sum(len(chunk[1][0]) for chunk in block), + np.sum([len(chunk[1][0]) for chunk in block]), self.model.size, ), ) From 547d401f03ef2fa046c8a10cde7ef7abd3f0fee4 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 13 Jun 2024 08:44:15 -0700 Subject: [PATCH 452/455] Try reverting ns.receiver module --- .../natural_source/receivers.py | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index 728552a9d8..7c6eed5207 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -1,8 +1,8 @@ -from ...utils.code_utils import validate_string +from ...utils.code_utils import deprecate_class, validate_string import numpy as np from scipy.constants import mu_0 -from scipy.sparse import csr_matrix +import scipy.sparse as sp from ...survey import BaseRx @@ -293,10 +293,8 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals v = -1j * v # Work backwards! - - gtop_v = v / bot - gbot_v = -imp * v / bot - n_d = self.nD + gtop_v = np.c_[v] / bot[:, None] + gbot_v = -imp[:, None] * np.c_[v] / bot[:, None] if mesh.dim == 3: ghx_v = np.einsum( @@ -317,15 +315,8 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals else: ghx_v -= gh_v - if v.ndim == 2: - # collapse into a long list of n_d vectors - ghx_v = csr_matrix(ghx_v.reshape((n_d, -1))) - ghy_v = csr_matrix(ghy_v.reshape((n_d, -1))) - ge_v = csr_matrix(ge_v.reshape((n_d, -1))) - - gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v - ge_v = Pe.T @ ge_v - + gh_v = Phx.T @ sp.csr_matrix(ghx_v) + Phy.T @ sp.csr_matrix(ghy_v) + ge_v = Pe.T @ sp.csr_matrix(ge_v) else: if mesh.dim == 1 and self.orientation != f.field_directions: gbot_v = -gbot_v @@ -565,9 +556,9 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): ghx_v += gh_v gh_v = ( - Phx.T @ csr_matrix(ghx_v) - + Phy.T @ csr_matrix(ghy_v) - + Phz.T @ csr_matrix(ghz_v) + Phx.T @ sp.csr_matrix(ghx_v) + + Phy.T @ sp.csr_matrix(ghy_v) + + Phz.T @ sp.csr_matrix(ghz_v) ) return f._hDeriv(src, None, gh_v, adjoint=True) @@ -653,3 +644,23 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): if adjoint: return imp_deriv return getattr(imp_deriv, self.component) + + +############ +# Deprecated +############ + + +@deprecate_class(removal_version="0.19.0", error=True) +class Point_impedance1D(PointNaturalSource): + pass + + +@deprecate_class(removal_version="0.19.0", error=True) +class Point_impedance3D(PointNaturalSource): + pass + + +@deprecate_class(removal_version="0.19.0", error=True) +class Point_tipper3D(Point3DTipper): + pass From 43b854af479ae2f7f5904e2371f83eaac72b74a4 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 13 Jun 2024 21:36:14 -0700 Subject: [PATCH 453/455] Revert changes to mat_mul --- simpeg/base/pde_simulation.py | 95 ++++++++++++----------------------- 1 file changed, 31 insertions(+), 64 deletions(-) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index f8739dcef3..7cd6db6e83 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -8,75 +8,42 @@ def __inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) - - if sp.issparse(M): - if v is not None: - if v.ndim > 1: - v = np.squeeze(v) - if u.ndim > 1: + if v is not None: + if u.ndim > 1: + if u.shape[1] > 1 and not isinstance( + u, (sp.csr_matrix, sp.csc_matrix, sp.coo_matrix) + ): # u has multiple fields if v.ndim == 1: - v = v[:, None] - if adjoint and v.shape[1] != u.shape[1] and v.shape[1] > 1: - # make sure v is a good shape - v = v.reshape(u.shape[0], -1, u.shape[1]) - else: - if v.ndim > 1: - u = u[:, None] - if v.ndim > 2: - u = u[:, None, :] - if adjoint: - if u.ndim > 1 and u.shape[-1] > 1: - return M.T * (u * v).sum(axis=-1) - return M.T * (u * v) - if u.ndim > 1 and u.shape[1] > 1: - return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) - return u * (M * v) - - else: - if u.ndim > 1: - UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + v = sp.diags(v) else: - U = sp.diags(u, format="csr") - UM = U @ M - if adjoint: - return UM.T - return UM - elif isinstance(M, tuple): - # assume it was a tuple of M_func, prop_deriv - M_deriv_func, prop_deriv = M + u = u[:, 0] + if u.ndim == 1: + if v.ndim > 1: + if v.shape[1] == 1: + v = v[:, 0] + else: + u = sp.diags(u) + if v.ndim > 2: + u = u[:, :, None] + if adjoint: + if u.ndim > 1 and u.shape[1] > 1 and not isinstance(u, sp.dia_matrix): + return M.T * ( + u[:, None, :] * v.reshape((u.shape[0], -1, u.shape[1])) + ).sum(axis=2) + return M.T * (u * v) + if u.ndim > 1 and u.shape[1] > 1: + return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) + return u * (M * v) + else: if u.ndim > 1: - Mu = [M_deriv_func(u[:, i]) for i in range(u.shape[1])] - if v is None: - Mu = sp.vstack([M @ prop_deriv for M in Mu]) - if adjoint: - return Mu.T - return Mu - elif v.ndim > 1: - v = np.squeeze(v) - if adjoint: - return sum( - [prop_deriv.T @ (Mu[i].T @ v[..., i]) for i in range(u.shape[1])] - ) - pv = prop_deriv @ v - return np.stack([M @ pv for M in Mu], axis=-1) + UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) else: - Mu = M_deriv_func(u) - if v is None: - Mu = Mu @ prop_deriv - if adjoint: - return Mu.T - return Mu - elif v.ndim > 1: - v = np.squeeze(v) - if adjoint: - return prop_deriv.T @ (Mu.T @ v) - return Mu @ (prop_deriv @ v) - else: - raise TypeError( - "The stashed property derivative is an unexpected type. Expected either a `tuple` or a " - f"sparse matrix. Received a {type(M)}." - ) + U = sp.diags(u, format="csr") + UM = U @ M + if adjoint: + return UM.T + return UM def with_property_mass_matrices(property_name): From da72d60afd2545011adeec0306a0ca31a127adcd Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 14 Jun 2024 07:48:07 -0700 Subject: [PATCH 454/455] Pass the source --- simpeg/electromagnetics/frequency_domain/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index de1afecb68..fc99133600 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -881,7 +881,7 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): C = self.mesh.edge_curl MfMui = self.MfMui - s_m, s_e = self.getSourceTerm(freq) + s_m, s_e = self.getSourceTerm(freq, source=src) s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) MfMuiDeriv = self.MfMuiDeriv(s_m) From 63312bd54e32178d09a0a86e62923daa1a5ad35e Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 14 Jun 2024 07:49:12 -0700 Subject: [PATCH 455/455] Revert "Revert changes to mat_mul" This reverts commit 43b854af479ae2f7f5904e2371f83eaac72b74a4. --- simpeg/base/pde_simulation.py | 95 +++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index 7cd6db6e83..f8739dcef3 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -8,42 +8,75 @@ def __inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) - if v is not None: - if u.ndim > 1: - if u.shape[1] > 1 and not isinstance( - u, (sp.csr_matrix, sp.csc_matrix, sp.coo_matrix) - ): + + if sp.issparse(M): + if v is not None: + if v.ndim > 1: + v = np.squeeze(v) + if u.ndim > 1: # u has multiple fields if v.ndim == 1: - v = sp.diags(v) + v = v[:, None] + if adjoint and v.shape[1] != u.shape[1] and v.shape[1] > 1: + # make sure v is a good shape + v = v.reshape(u.shape[0], -1, u.shape[1]) else: - u = u[:, 0] - if u.ndim == 1: - if v.ndim > 1: - if v.shape[1] == 1: - v = v[:, 0] - else: - u = sp.diags(u) - if v.ndim > 2: - u = u[:, :, None] - if adjoint: - if u.ndim > 1 and u.shape[1] > 1 and not isinstance(u, sp.dia_matrix): - return M.T * ( - u[:, None, :] * v.reshape((u.shape[0], -1, u.shape[1])) - ).sum(axis=2) - return M.T * (u * v) - if u.ndim > 1 and u.shape[1] > 1: - return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) - return u * (M * v) - else: + if v.ndim > 1: + u = u[:, None] + if v.ndim > 2: + u = u[:, None, :] + if adjoint: + if u.ndim > 1 and u.shape[-1] > 1: + return M.T * (u * v).sum(axis=-1) + return M.T * (u * v) + if u.ndim > 1 and u.shape[1] > 1: + return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) + return u * (M * v) + + else: + if u.ndim > 1: + UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + else: + U = sp.diags(u, format="csr") + UM = U @ M + if adjoint: + return UM.T + return UM + elif isinstance(M, tuple): + # assume it was a tuple of M_func, prop_deriv + M_deriv_func, prop_deriv = M if u.ndim > 1: - UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + Mu = [M_deriv_func(u[:, i]) for i in range(u.shape[1])] + if v is None: + Mu = sp.vstack([M @ prop_deriv for M in Mu]) + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return sum( + [prop_deriv.T @ (Mu[i].T @ v[..., i]) for i in range(u.shape[1])] + ) + pv = prop_deriv @ v + return np.stack([M @ pv for M in Mu], axis=-1) else: - U = sp.diags(u, format="csr") - UM = U @ M - if adjoint: - return UM.T - return UM + Mu = M_deriv_func(u) + if v is None: + Mu = Mu @ prop_deriv + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return prop_deriv.T @ (Mu.T @ v) + return Mu @ (prop_deriv @ v) + else: + raise TypeError( + "The stashed property derivative is an unexpected type. Expected either a `tuple` or a " + f"sparse matrix. Received a {type(M)}." + ) def with_property_mass_matrices(property_name):