From fd060670b9e6d95348a40552495cde5fabe9c9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Sun, 22 Aug 2021 12:18:48 +0200 Subject: [PATCH] Remove optimize (#245) * Remove optimize * Move `expand_model` and `estimate_gridding_options` out from simulations * `Model.interpolate_to_grid()`: Don't interpolate if same grid --- CHANGELOG.rst | 23 +- docs/api/index.rst | 1 - docs/api/optimize.rst | 6 - emg3d/meshes.py | 264 ++++++++++++++++- emg3d/models.py | 89 +++++- emg3d/optimize.py | 233 +-------------- emg3d/simulations.py | 593 +++++++++++++++----------------------- tests/test_meshes.py | 155 ++++++++++ tests/test_models.py | 42 +++ tests/test_optimize.py | 201 ------------- tests/test_simulations.py | 331 +++++++++++---------- 11 files changed, 988 insertions(+), 950 deletions(-) delete mode 100644 docs/api/optimize.rst delete mode 100644 tests/test_optimize.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9dae365a..174d85c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,27 @@ Changelog """""""""" -latest ------- +v1.2.1: Remove optimize & bug fix +--------------------------------- -- Simulation: +**2021-08-22** + +- ``io``: Adjustment so that hdf5 tracks the order of dicts. + +- ``simulations``: - Adjust printing: correct simulation results for adjusted solver printing - levels; *default solver verbosity is new 1*; ``log`` can now be overwritten - in ``solver_opts`` (mainly for debugging). + levels; **default solver verbosity is new 1**; ``log`` can now be + overwritten in ``solver_opts`` (mainly for debugging). -- Bug fixes: + - Functions moved out of ``simulations``: ``expand_grid_model`` moved to + ``models`` and ``estimate_gridding_options`` to ``meshes``. The + availability of these functions through ``simulations`` will be removed in + v1.4.0. - - Track order when saving to hdf5. +- ``optimize``: the module is deprecated and will be removed in v1.4.0. The two + functions ``optimize.{misfit;gradient}`` are embedded directly in + ``Simulation.{misfit;gradient}``. v1.2.0: White noise diff --git a/docs/api/index.rst b/docs/api/index.rst index 23bcad2e..0df2b064 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -22,7 +22,6 @@ API reference maps meshes models - optimize simulations solver surveys diff --git a/docs/api/optimize.rst b/docs/api/optimize.rst deleted file mode 100644 index 85eefa1f..00000000 --- a/docs/api/optimize.rst +++ /dev/null @@ -1,6 +0,0 @@ -Optimize -======== - -.. automodapi:: emg3d.optimize - :no-inheritance-diagram: - :no-heading: diff --git a/emg3d/meshes.py b/emg3d/meshes.py index a8d53492..ab62e434 100644 --- a/emg3d/meshes.py +++ b/emg3d/meshes.py @@ -32,7 +32,7 @@ __all__ = ['TensorMesh', 'BaseMesh', 'construct_mesh', 'origin_and_widths', 'good_mg_cell_nr', 'skin_depth', 'wavelength', 'cell_width', - 'check_mesh'] + 'check_mesh', 'estimate_gridding_opts'] class BaseMesh: @@ -1054,3 +1054,265 @@ def check_mesh(mesh): f"MG solver. Good numbers are:\n{good_mg_cell_nr(max_nr=5000)}" ) warnings.warn(msg, UserWarning) + + +def estimate_gridding_opts(gridding_opts, model, survey, input_sc2=None): + """Estimate parameters for automatic gridding. + + Automatically determines the required gridding options from the provided + model, and survey, if they are not provided in ``gridding_opts``. + + The dict ``gridding_opts`` can contain any input parameter taken by + :func:`emg3d.meshes.construct_mesh`, see the corresponding documentation + for more details with regards to the possibilities. + + Different keys of ``gridding_opts`` are treated differently: + + - The following parameters are estimated from the ``model`` if not + provided: + + - ``properties``: lowest conductivity / highest resistivity in the + outermost layer in a given direction. This is usually air in x/y and + positive z. Note: This is very conservative. If you go into deeper + water you could provide less conservative values. + - ``mapping``: taken from model. + + - The following parameters are estimated from the ``survey`` if not + provided: + + - ``frequency``: average (on log10-scale) of all frequencies. + - ``center``: center of all sources. + - ``domain``: from ``vector`` or ``distance``, if provided, or + + - in x/y-directions: extent of sources and receivers plus 10% on each + side, ensuring ratio of 3. + - in z-direction: extent of sources and receivers, ensuring ratio of 2 + to horizontal dimension; 1/10 tenth up, 9/10 down. + + The ratio means that it is enforced that the survey dimension in x or + y-direction is not smaller than a third of the survey dimension in the + other direction. If not, the smaller dimension is expanded + symmetrically. Similarly in the vertical direction, which must be at + least half the dimension of the maximum horizontal dimension or 5 km, + whatever is smaller. Otherwise it is expanded in a ratio of 9 parts + downwards, one part upwards. + + - The following parameter is taken from the ``grid`` if provided as a + string: + + - ``vector``: This is the only real "difference" to the inputs of + :func:`emg3d.meshes.construct_mesh`. The normal input is accepted, but + it can also be a string containing any combination of ``'x'``, ``'y'``, + and ``'z'``. All directions contained in this string are then taken + from the provided grid. E.g., if ``gridding_opts['vector']='xz'`` it + will take the x- and z-directed vectors from the grid. + + - The following parameters are simply passed along if they are provided, + nothing is done otherwise: + + - ``vector`` + - ``distance`` + - ``stretching`` + - ``seasurface`` + - ``cell_numbers`` + - ``lambda_factor`` + - ``lambda_from_center`` + - ``max_buffer`` + - ``min_width_limits`` + - ``min_width_pps`` + - ``verb`` + + + Parameters + ---------- + gridding_opts : dict + Containing input parameters to provide to + :func:`emg3d.meshes.construct_mesh`. See the corresponding + documentation and the explanations above. + + model : Model + The model; a :class:`emg3d.models.Model` instance. + + survey : Survey + The survey; a :class:`emg3d.surveys.Survey` instance. + + input_sc2 : int, default: None + If :func:`emg3d.models.expand_grid_model` was used, ``input_sc2`` + corresponds to the original ``grid.shape_cells[2]``. + + + Returns + ------- + gridding_opts : dict + Dict to provide to :func:`emg3d.meshes.construct_mesh`. + + """ + # Initiate new gridding_opts. + gopts = {} + grid = model.grid + + # Optional values that we only include if provided. + for name in ['seasurface', 'cell_numbers', 'lambda_factor', + 'lambda_from_center', 'max_buffer', 'verb']: + if name in gridding_opts.keys(): + gopts[name] = gridding_opts.pop(name) + for name in ['stretching', 'min_width_limits', 'min_width_pps']: + if name in gridding_opts.keys(): + value = gridding_opts.pop(name) + if isinstance(value, (list, tuple)) and len(value) == 3: + value = {'x': value[0], 'y': value[1], 'z': value[2]} + gopts[name] = value + + # Mapping defaults to model map. + gopts['mapping'] = gridding_opts.pop('mapping', model.map) + if not isinstance(gopts['mapping'], str): + gopts['mapping'] = gopts['mapping'].name + + # Frequency defaults to average frequency (log10). + frequency = 10**np.mean(np.log10([v for v in survey.frequencies.values()])) + gopts['frequency'] = gridding_opts.pop('frequency', frequency) + + # Center defaults to center of all sources. + center = np.array([s.center for s in survey.sources.values()]).mean(0) + gopts['center'] = gridding_opts.pop('center', center) + + # Vector. + vector = gridding_opts.pop('vector', None) + if isinstance(vector, str): + # If vector is a string we take the corresponding vectors from grid. + vector = ( + grid.nodes_x if 'x' in vector.lower() else None, + grid.nodes_y if 'y' in vector.lower() else None, + grid.nodes_z[:input_sc2] if 'z' in vector.lower() else None, + ) + gopts['vector'] = vector + if isinstance(vector, dict): + vector = (vector['x'], vector['y'], vector['z']) + elif vector is not None and len(vector) == 3: + gopts['vector'] = {'x': vector[0], 'y': vector[1], 'z': vector[2]} + + # Distance. + distance = gridding_opts.pop('distance', None) + gopts['distance'] = distance + if isinstance(distance, dict): + distance = (distance['x'], distance['y'], distance['z']) + elif distance is not None and len(distance) == 3: + gopts['distance'] = {'x': distance[0], 'y': distance[1], + 'z': distance[2]} + + # Properties defaults to lowest conductivities (AFTER model expansion). + properties = gridding_opts.pop('properties', None) + if properties is None: + + # Get map (in principle the map in gridding_opts could be different + # from the map in the model). + m = gopts['mapping'] + if isinstance(m, str): + m = getattr(maps, 'Map'+m)() + + # Minimum conductivity of all values (x, y, z). + def get_min(ix, iy, iz): + """Get minimum: very conservative/costly, but avoiding problems.""" + + # Collect all x (y, z) values. + data = np.array([]) + for p in ['x', 'y', 'z']: + prop = getattr(model, 'property_'+p) + if prop is not None: + prop = model.map.backward(prop[ix, iy, iz]) + data = np.r_[data, np.min(prop)] + + # Return minimum conductivity (on mapping). + return m.forward(min(data)) + + # Buffer properties. + xneg = get_min(0, slice(None), slice(None)) + xpos = get_min(-1, slice(None), slice(None)) + yneg = get_min(slice(None), 0, slice(None)) + ypos = get_min(slice(None), -1, slice(None)) + zneg = get_min(slice(None), slice(None), 0) + zpos = get_min(slice(None), slice(None), -1) + + # Source property. + ix = np.argmin(abs(grid.nodes_x - gopts['center'][0])) + iy = np.argmin(abs(grid.nodes_y - gopts['center'][1])) + iz = np.argmin(abs(grid.nodes_z - gopts['center'][2])) + source = get_min(ix, iy, iz) + + properties = [source, xneg, xpos, yneg, ypos, zneg, zpos] + + gopts['properties'] = properties + + # Domain; default taken from survey. + domain = gridding_opts.pop('domain', None) + if isinstance(domain, dict): + domain = (domain['x'], domain['y'], domain['z']) + + def get_dim_diff(i): + """Return ([min, max], dim) of inp. + + Take it from domain if provided, else from vector if provided, else + from survey, adding 10% on each side). + """ + if domain is not None and domain[i] is not None: + # domain is provided. + dim = domain[i] + diff = np.diff(dim)[0] + get_it = False + + elif vector is not None and vector[i] is not None: + # vector is provided. + dim = [np.min(vector[i]), np.max(vector[i])] + diff = np.diff(dim)[0] + get_it = False + + elif distance is not None and distance[i] is not None: + # distance is provided. + dim = None + diff = abs(distance[i][0]) + abs(distance[i][1]) + get_it = False + + else: + # Get it from survey, add 5 % on each side. + inp = np.array([s.center[i] for s in survey.sources.values()]) + for s in survey.sources.values(): + inp = np.r_[inp, [r.center_abs(s)[i] + for r in survey.receivers.values()]] + dim = [min(inp), max(inp)] + diff = np.diff(dim)[0] + dim = [min(inp)-diff/10, max(inp)+diff/10] + diff = np.diff(dim)[0] + get_it = True + + diff = np.where(diff > 1e-9, diff, 1e-9) # Avoid division by 0 later + return dim, diff, get_it + + xdim, xdiff, get_x = get_dim_diff(0) + ydim, ydiff, get_y = get_dim_diff(1) + zdim, zdiff, get_z = get_dim_diff(2) + + # Ensure the ratio xdim:ydim is at most 3. + if get_y and xdiff/ydiff > 3: + diff = round((xdiff/3.0 - ydiff)/2.0) + ydim = [ydim[0]-diff, ydim[1]+diff] + elif get_x and ydiff/xdiff > 3: + diff = round((ydiff/3.0 - xdiff)/2.0) + xdim = [xdim[0]-diff, xdim[1]+diff] + + # Ensure the ratio zdim:horizontal is at most 2. + hdist = min(10000, max(xdiff, ydiff)) + if get_z and hdist/zdiff > 2: + diff = round((hdist/2.0 - zdiff)/10.0) + zdim = [zdim[0]-9*diff, zdim[1]+diff] + + # Collect + gopts['domain'] = {'x': xdim, 'y': ydim, 'z': zdim} + + # Ensure no gridding_opts left. + if gridding_opts: + raise TypeError( + f"Unexpected gridding_opts: {list(gridding_opts.keys())}." + ) + + # Return gridding_opts. + return gopts diff --git a/emg3d/models.py b/emg3d/models.py index 0cf728bf..dc9a104f 100644 --- a/emg3d/models.py +++ b/emg3d/models.py @@ -24,7 +24,7 @@ from emg3d import maps, meshes, utils -__all__ = ['Model', 'VolumeModel'] +__all__ = ['Model', 'VolumeModel', 'expand_grid_model'] # MODEL @@ -324,6 +324,9 @@ def interpolate_to_grid(self, grid, **interpolate_opts): A new :class:`emg3d.models.Model` instance on ``grid``. """ + # If grids are identical, return a copy. + if grid == self.grid: + return self.copy() # Get solver options, set to defaults if not provided. g2g_inp = { @@ -523,3 +526,87 @@ def eta_z(self): def zeta(self): r"""Volume-averaged, isotropic zeta.""" return self._zeta + + +def expand_grid_model(model, expand, interface): + """Expand model and grid according to provided parameters. + + Expand the grid and corresponding model in positive z-direction from the + edge of the grid to the interface with property ``expand[0]``, and a 100 m + thick layer above the interface with property ``expand[1]``. + + The provided properties are taken as isotropic (as is the case in water and + air); ``mu_r`` and ``epsilon_r`` are expanded with ones, if necessary. + + The ``interface`` is usually the sea-surface, and ``expand`` is therefore + ``[property_sea, property_air]``. + + Parameters + ---------- + model : Model + The model; a :class:`emg3d.models.Model` instance. + + expand : list + The two properties below and above the interface: + ``[below_interface, above_interface]``. + + interface : float + Interface between the two properties in ``expand``. + + + Returns + ------- + exp_grid : TensorMesh + Expanded grid; a :class:`emg3d.meshes.TensorMesh` instance. + + exp_model : Model + The expanded model; a :class:`emg3d.models.Model` instance. + + """ + grid = model.grid + + def extend_property(prop, add_values, nadd): + """Expand property `model.prop`, IF it is not None.""" + + if getattr(model, prop) is None: + prop_ext = None + + else: + prop_ext = np.zeros((grid.shape_cells[0], grid.shape_cells[1], + grid.shape_cells[2]+nadd)) + prop_ext[:, :, :-nadd] = getattr(model, prop) + if nadd == 2: + prop_ext[:, :, -2] = add_values[0] + prop_ext[:, :, -1] = add_values[1] + + return prop_ext + + # Initiate. + nzadd = 0 + hz_ext = grid.h[2] + + # Fill-up property_below. + if grid.nodes_z[-1] < interface-0.05: # At least 5 cm. + hz_ext = np.r_[hz_ext, interface-grid.nodes_z[-1]] + nzadd += 1 + + # Add 100 m of property_above. + if grid.nodes_z[-1] <= interface+0.001: # +1mm + hz_ext = np.r_[hz_ext, 100] + nzadd += 1 + + if nzadd > 0: + # Extend properties. + property_x = extend_property('property_x', expand, nzadd) + property_y = extend_property('property_y', expand, nzadd) + property_z = extend_property('property_z', expand, nzadd) + mu_r = extend_property('mu_r', [1, 1], nzadd) + epsilon_r = extend_property('epsilon_r', [1, 1], nzadd) + + # Create extended grid and model. + grid = meshes.TensorMesh( + [grid.h[0], grid.h[1], hz_ext], origin=grid.origin) + model = Model(grid, property_x, property_y, property_z, mu_r, + epsilon_r, mapping=model.map.name) + + return model diff --git a/emg3d/optimize.py b/emg3d/optimize.py index fea89d12..f79589a4 100644 --- a/emg3d/optimize.py +++ b/emg3d/optimize.py @@ -1,6 +1,8 @@ """ Functionalities related to optimization (minimization, inversion), such as the misfit function and its gradient. + +DEPRECATED, will be removed from v1.4.0 onwards. """ # Copyright 2018-2021 The emsig community. # @@ -18,229 +20,20 @@ # License for the specific language governing permissions and limitations under # the License. -import numpy as np - -from emg3d import maps, fields - -__all__ = ['misfit', 'gradient'] +import warnings def misfit(simulation): - r"""Misfit or cost function. - - The data misfit or weighted least-squares functional using an :math:`l_2` - norm is given by - - .. math:: - :label: misfit - - \phi = \frac{1}{2} \sum_s\sum_r\sum_f - \left\lVert - W_{s,r,f} \left( - \textbf{d}_{s,r,f}^\text{pred} - -\textbf{d}_{s,r,f}^\text{obs} - \right) \right\rVert^2 \, , - - where :math:`s, r, f` stand for source, receiver, and frequency, - respectively; :math:`\textbf{d}^\text{obs}` are the observed electric and - magnetic data, and :math:`\textbf{d}^\text{pred}` are the synthetic - electric and magnetic data. As of now the misfit does not include any - regularization term. - - The data weight of observation :math:`d_i` is given by :math:`W_i = - \varsigma^{-1}_i`, where :math:`\varsigma_i` is the standard deviation of - the observation, see :attr:`emg3d.surveys.Survey.standard_deviation`. - - .. note:: - - You can easily implement your own misfit function (to include, e.g., a - regularization term) by monkey patching this misfit function with your - own:: - - def my_misfit_function(simulation): - '''Returns the misfit as a float.''' - - # Computing the misfit... - - return misfit - - # Monkey patch optimize.misfit: - emg3d.optimize.misfit = my_misfit_function - - # And now all the regular stuff, initiate a Simulation etc - simulation = emg3d.Simulation(survey, grid, model) - simulation.misfit - # => will return your misfit - # (will also be used for the adjoint-state gradient). - - - Parameters - ---------- - simulation : Simulation - The simulation; a :class:`emg3d.simulations.Simulation` instance. - - - Returns - ------- - misfit : float - Value of the misfit function. - - """ - - # Check if electric fields have already been computed. - test_efield = sum([1 if simulation._dict_efield[src][freq] is None else 0 - for src, freq in simulation._srcfreq]) - if test_efield: - simulation.compute() - - # Check if weights are stored already. - # (weights are currently simply 1/std^2; but might change in the future). - if 'weights' not in simulation.data.keys(): - - # Get standard deviation, raise warning if not set. - std = simulation.survey.standard_deviation - if std is None: - raise ValueError( - "Either `noise_floor` or `relative_error` or both must " - "be provided (>0) to compute the `standard_deviation`. " - "It can also be set directly (same shape as data). " - "The standard deviation is required to compute the misfit." - ) - - # Store weights - simulation.data['weights'] = std**-2 - - # Calculate and store residual. - residual = simulation.data.synthetic - simulation.data.observed - simulation.data['residual'] = residual - - # Get weights, calculate misfit. - weights = simulation.data['weights'] - misfit = np.sum(weights*(residual.conj()*residual)).real/2 - - return misfit.data + """Deprecated, moved directly to `emg3d.Simulation.misfit`.""" + msg = ("emg3d: `optimize` is deprecated and will be removed in v1.4.0." + "`optimize.misfit` is embedded directly in Simulation.misfit`.") + warnings.warn(msg, FutureWarning) + return simulation.misfit def gradient(simulation): - r"""Compute the discrete gradient using the adjoint-state method. - - The discrete adjoint-state gradient for a single source at a single - frequency is given by Equation (10) in [PlMu08]_, - - .. math:: - - \nabla_p \phi(\textbf{p}) = - -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{x; k+\frac{1}{2}, l, m} - \frac{\partial S_{k+\frac{1}{2}, l, m}}{\partial \textbf{p}} - \textbf{E}_{x; k+\frac{1}{2}, l, m}\\ - -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{y; k, l+\frac{1}{2}, m} - \frac{\partial S_{k, l+\frac{1}{2}, m}}{\partial \textbf{p}} - \textbf{E}_{y; k, l+\frac{1}{2}, m}\\ - -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{z; k, l, m+\frac{1}{2}} - \frac{\partial S_{k, l, m+\frac{1}{2}}}{\partial \textbf{p}} - \textbf{E}_{z; k, l, m+\frac{1}{2}}\, , - - - - where :math:`\textbf{E}` is the electric (forward) field and - :math:`\mathbf{\lambda}` is the back-propagated residual field (from - electric and magnetic receivers); :math:`\bar{~}` denotes conjugate. - The :math:`\partial S`-part takes care of the volume-averaged model - parameters. - - .. warning:: - - To obtain the proper adjoint-state gradient you have to choose linear - interpolation for the receiver responses: - ``emg3d.Simulation(..., receiver_interpolation='linear')``. - The reason is that the point-source is the adjoint of a tri-linear - interpolation, so the residual should come from a linear interpolation. - - Also, the adjoint test for magnetic receivers does not yet pass. - Electric receivers are good to go. - - .. note:: - - The currently implemented gradient is only for isotropic models without - relative electric permittivity nor relative magnetic permeability. - - - Parameters - ---------- - simulation : Simulation - The simulation; a :class:`emg3d.simulations.Simulation` instance. - - - Returns - ------- - grad : ndarray - Adjoint-state gradient (same shape as ``simulation.model``). - - """ - - # Check limitation 1: So far only isotropic models. - if simulation.model.case != 'isotropic': - raise NotImplementedError( - "Gradient only implemented for isotropic models." - ) - - # Check limitation 2: No epsilon_r, mu_r. - var = (simulation.model.epsilon_r, simulation.model.mu_r) - for v, n in zip(var, ('el. permittivity', 'magn. permeability')): - if v is not None and not np.allclose(v, 1.0): - raise NotImplementedError(f"Gradient not implemented for {n}.") - - # Ensure misfit has been computed (and therefore the electric fields). - _ = simulation.misfit - - # Compute back-propagating electric fields. - simulation._bcompute() - - # Pre-allocate the gradient on the mesh. - gradient_model = np.zeros(simulation.model.grid.shape_cells, order='F') - - # Loop over source-frequency pairs. - for src, freq in simulation._srcfreq: - - # Multiply forward field with backward field; take real part. - # This is the actual Equation (10), with: - # del S / del p = iwu0 V sigma / sigma, - # where lambda and E are already volume averaged. - efield = simulation._dict_efield[src][freq] # Forward electric field - bfield = simulation._dict_bfield[src][freq] # Conj. backprop. field - gfield = fields.Field( - grid=efield.grid, - data=-np.real(bfield.field * efield.smu0 * efield.field), - dtype=float, - ) - - # Bring the gradient back from the computation grid to the model grid. - gradient = gfield.interpolate_to_grid(simulation.model.grid) - - # Pre-allocate the gradient for this src-freq. - shape = gradient.grid.shape_cells - grad_x = np.zeros(shape, order='F') - grad_y = np.zeros(shape, order='F') - grad_z = np.zeros(shape, order='F') - - # Map the field to cell centers times volume. - cell_volumes = gradient.grid.cell_volumes.reshape(shape, order='F') - maps.interp_edges_to_vol_averages( - ex=gradient.fx, ey=gradient.fy, ez=gradient.fz, - volumes=cell_volumes, - ox=grad_x, oy=grad_y, oz=grad_z) - grad = grad_x + grad_y + grad_z - - # => Frequency-dependent depth-weighting should go here. - - # Add this src-freq gradient to the total gradient. - gradient_model -= grad - - # => Frequency-independent depth-weighting should go here. - - # Apply derivative-chain of property-map - # (only relevant if `mapping` is something else than conductivity). - simulation.model.map.derivative_chain( - gradient_model, simulation.model.property_x) - - return gradient_model + """Deprecated, moved directly to `emg3d.Simulations.gradient`.""" + msg = ("emg3d: `optimize` is deprecated and will be removed in v1.4.0." + "`optimize.gradient` is embedded directly in Simulation.gradient`.") + warnings.warn(msg, FutureWarning) + return simulation.gradient diff --git a/emg3d/simulations.py b/emg3d/simulations.py index bf6aacf5..847b87f1 100644 --- a/emg3d/simulations.py +++ b/emg3d/simulations.py @@ -27,10 +27,10 @@ import numpy as np -from emg3d import (electrodes, fields, io, maps, meshes, models, - optimize, solver, surveys, utils) +from emg3d import (electrodes, fields, io, maps, meshes, models, solver, + surveys, utils) -__all__ = ['Simulation', 'expand_grid_model', 'estimate_gridding_opts'] +__all__ = ['Simulation', ] @utils._known_class @@ -121,7 +121,7 @@ class Simulation: ``'both``' is passed to :func:`emg3d.meshes.construct_mesh`; consult the corresponding documentation for more information. Parameters that are not provided are estimated from the provided model, grid, and - survey using :func:`emg3d.simulations.estimate_gridding_opts`, which + survey using :func:`emg3d.meshes.estimate_gridding_opts`, which documentation contains more information too. There are two notably differences to the parameters described in @@ -130,12 +130,12 @@ class Simulation: - ``vector``: besides the normal possibility it can also be a string containing one or several of ``'x'``, ``'y'``, and ``'z'``. In these cases the corresponding dimension of the input mesh is provided as - vector. See :func:`emg3d.simulations.estimate_gridding_opts`. + vector. See :func:`emg3d.meshes.estimate_gridding_opts`. - ``expand``: in the format of ``[property_sea, property_air]``; if provided, the input model is expanded up to the seasurface with sea water, and an air layer is added. The actual height of the seasurface can be defined with the key ``seasurface``. See - :func:`emg3d.simulations.expand_grid_model`. + :func:`emg3d.models.expand_grid_model`. solver_opts : dict, default: {'verb': 1'} Passed through to :func:`emg3d.solver.solve`. The dict can contain any @@ -848,14 +848,60 @@ def compute(self, observed=False, **kwargs): # OPTIMIZATION @property def gradient(self): - """Return the gradient of the misfit function. + r"""Compute the discrete gradient using the adjoint-state method. - See :func:`emg3d.optimize.gradient`. + The discrete adjoint-state gradient for a single source at a single + frequency is given by Equation (10) in [PlMu08]_, + + .. math:: + + \nabla_p \phi(\textbf{p}) = + -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{x; k+\frac{1}{2}, l, m} + \frac{\partial S_{k+\frac{1}{2}, l, m}}{\partial \textbf{p}} + \textbf{E}_{x; k+\frac{1}{2}, l, m}\\ + -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{y; k, l+\frac{1}{2}, m} + \frac{\partial S_{k, l+\frac{1}{2}, m}}{\partial \textbf{p}} + \textbf{E}_{y; k, l+\frac{1}{2}, m}\\ + -&\sum_{k,l,m}\mathbf{\bar{\lambda}}_{z; k, l, m+\frac{1}{2}} + \frac{\partial S_{k, l, m+\frac{1}{2}}}{\partial \textbf{p}} + \textbf{E}_{z; k, l, m+\frac{1}{2}}\, , + + + where :math:`\textbf{E}` is the electric (forward) field and + :math:`\mathbf{\lambda}` is the back-propagated residual field (from + electric and magnetic receivers); :math:`\bar{~}` denotes conjugate. + The :math:`\partial S`-part takes care of the volume-averaged model + parameters. + + .. warning:: + + To obtain the proper adjoint-state gradient you have to choose + linear interpolation for the receiver responses: + ``emg3d.Simulation(..., receiver_interpolation='linear')``. The + reason is that the point-source is the adjoint of a tri-linear + interpolation, so the residual should come from a linear + interpolation. + + Also, the adjoint test for magnetic receivers does not yet pass. + Electric receivers are good to go. + + .. note:: + + The currently implemented gradient is only for isotropic models + without relative electric permittivity nor relative magnetic + permeability. + + + Returns + ------- + grad : ndarray + Adjoint-state gradient (same shape as ``simulation.model``). """ if self._gradient is None: + + # Warn that cubic is not good for adjoint-state gradient. if self.receiver_interpolation == 'cubic': - # Warn that cubic is not good for adjoint-state gradient. msg = ( "emg3d: Receiver responses were obtained with cubic " "interpolation. This will not yield the exact gradient. " @@ -863,18 +909,180 @@ def gradient(self): "Simulation()." ) warnings.warn(msg, UserWarning) - self._gradient = optimize.gradient(self) + + # Check limitation 1: So far only isotropic models. + if self.model.case != 'isotropic': + raise NotImplementedError( + "Gradient only implemented for isotropic models." + ) + + # Check limitation 2: No epsilon_r, mu_r. + var = (self.model.epsilon_r, self.model.mu_r) + for v, n in zip(var, ('el. permittivity', 'magn. permeability')): + if v is not None and not np.allclose(v, 1.0): + raise NotImplementedError( + f"Gradient not implemented for {n}." + ) + + # Ensure misfit has been computed + # (and therefore the electric fields). + _ = self.misfit + + # Compute back-propagating electric fields. + self._bcompute() + + # Pre-allocate the gradient on the mesh. + gradient_model = np.zeros(self.model.grid.shape_cells, order='F') + + # Loop over source-frequency pairs. + for src, freq in self._srcfreq: + + # Multiply forward field with backward field; take real part. + # This is the actual Equation (10), with: + # del S / del p = iwu0 V sigma / sigma, + # where lambda and E are already volume averaged. + efield = self._dict_efield[src][freq] # Forward electric field + bfield = self._dict_bfield[src][freq] # Conj. backprop. field + gfield = fields.Field( + grid=efield.grid, + data=-np.real(bfield.field * efield.smu0 * efield.field), + dtype=float, + ) + + # Bring the gradient back from the computation grid to the + # model grid. + gradient = gfield.interpolate_to_grid(self.model.grid) + + # Pre-allocate the gradient for this src-freq. + shape = gradient.grid.shape_cells + grad_x = np.zeros(shape, order='F') + grad_y = np.zeros(shape, order='F') + grad_z = np.zeros(shape, order='F') + + # Map the field to cell centers times volume. + cell_volumes = gradient.grid.cell_volumes.reshape( + shape, order='F') + maps.interp_edges_to_vol_averages( + ex=gradient.fx, ey=gradient.fy, ez=gradient.fz, + volumes=cell_volumes, + ox=grad_x, oy=grad_y, oz=grad_z) + grad = grad_x + grad_y + grad_z + + # => Frequency-dependent depth-weighting should go here. + + # Add this src-freq gradient to the total gradient. + gradient_model -= grad + + # => Frequency-independent depth-weighting should go here. + + # Apply derivative-chain of property-map + # (only relevant if `mapping` is something else than conductivity). + self.model.map.derivative_chain( + gradient_model, self.model.property_x) + + self._gradient = gradient_model + return self._gradient[:, :, :self._input_sc2] @property def misfit(self): - """Return the misfit function. + r"""Misfit or cost function. + + The data misfit or weighted least-squares functional using an + :math:`l_2` norm is given by + + .. math:: + :label: misfit + + \phi = \frac{1}{2} \sum_s\sum_r\sum_f + \left\lVert + W_{s,r,f} \left( + \textbf{d}_{s,r,f}^\text{pred} + -\textbf{d}_{s,r,f}^\text{obs} + \right) \right\rVert^2 \, , + + where :math:`s, r, f` stand for source, receiver, and frequency, + respectively; :math:`\textbf{d}^\text{obs}` are the observed electric + and magnetic data, and :math:`\textbf{d}^\text{pred}` are the synthetic + electric and magnetic data. As of now the misfit does not include any + regularization term. - See :func:`emg3d.optimize.misfit`. + The data weight of observation :math:`d_i` is given by :math:`W_i = + \varsigma^{-1}_i`, where :math:`\varsigma_i` is the standard deviation + of the observation, see + :attr:`emg3d.surveys.Survey.standard_deviation`. + + .. note:: + + You can easily implement your own misfit function (to include, + e.g., a regularization term) by monkey patching this misfit + function with your own:: + + @property # misfit is a property + def my_misfit_function(self): + '''Returns the misfit as a float.''' + + if self._misfit is None: + self.compute() # Ensures fields are computed. + + # Computing your misfit... + self._misfit = your misfit + + return self._misfit + + # Monkey patch simulation.misfit: + emg3d.simulation.Simulation.misfit = my_misfit_function + + # And now all the regular stuff, initiate a Simulation etc + simulation = emg3d.Simulation(survey, grid, model) + simulation.misfit + # => will return your misfit + # (will also be used for the adjoint-state gradient). + + + Returns + ------- + misfit : float + Value of the misfit function. """ if self._misfit is None: - self._misfit = optimize.misfit(self) + + # Check if electric fields have already been computed. + test_efield = sum( + [1 if self._dict_efield[src][freq] is None else 0 + for src, freq in self._srcfreq] + ) + + if test_efield: + self.compute() + + # Check if weights are stored already. (weights are currently + # simply 1/std^2; but might change in the future). + if 'weights' not in self.data.keys(): + + # Get standard deviation, raise warning if not set. + std = self.survey.standard_deviation + if std is None: + raise ValueError( + "Either `noise_floor` or `relative_error` or both " + "must be provided (>0) to compute the " + "`standard_deviation`. It can also be set directly " + "(same shape as data). The standard deviation is " + "required to compute the misfit." + ) + + # Store weights + self.data['weights'] = std**-2 + + # Calculate and store residual. + residual = self.data.synthetic - self.data.observed + self.data['residual'] = residual + + # Get weights, calculate misfit. + weights = self.data['weights'] + self._misfit = np.sum(weights*(residual.conj()*residual)).real/2 + return self._misfit def _get_bfields(self, inp): @@ -1152,360 +1360,31 @@ def _set_model(self, model, kwargs): "`g_opts['expand']` is provided.") raise KeyError(msg) from e - model = expand_grid_model(model, expand, interface) + model = models.expand_grid_model(model, expand, interface) # Get automatic gridding input. # Estimate the parameters from survey and model if not provided. - gridding_opts = estimate_gridding_opts( + gridding_opts = meshes.estimate_gridding_opts( g_opts, model, self.survey, self._input_sc2) self.gridding_opts = gridding_opts self.model = model -# HELPER FUNCTIONS +# DEPRECATED, will be removed from v1.4.0 onwards. def expand_grid_model(model, expand, interface): - """Expand model and grid according to provided parameters. - - Expand the grid and corresponding model in positive z-direction from the - edge of the grid to the interface with property ``expand[0]``, and a 100 m - thick layer above the interface with property ``expand[1]``. - - The provided properties are taken as isotropic (as is the case in water and - air); ``mu_r`` and ``epsilon_r`` are expanded with ones, if necessary. - - The ``interface`` is usually the sea-surface, and ``expand`` is therefore - ``[property_sea, property_air]``. - - Parameters - ---------- - model : Model - The model; a :class:`emg3d.models.Model` instance. - - expand : list - The two properties below and above the interface: - ``[below_interface, above_interface]``. - - interface : float - Interface between the two properties in ``expand``. - - - Returns - ------- - exp_grid : TensorMesh - Expanded grid; a :class:`emg3d.meshes.TensorMesh` instance. - - exp_model : Model - The expanded model; a :class:`emg3d.models.Model` instance. - - """ - grid = model.grid - - def extend_property(prop, add_values, nadd): - """Expand property `model.prop`, IF it is not None.""" - - if getattr(model, prop) is None: - prop_ext = None - - else: - prop_ext = np.zeros((grid.shape_cells[0], grid.shape_cells[1], - grid.shape_cells[2]+nadd)) - prop_ext[:, :, :-nadd] = getattr(model, prop) - if nadd == 2: - prop_ext[:, :, -2] = add_values[0] - prop_ext[:, :, -1] = add_values[1] - - return prop_ext - - # Initiate. - nzadd = 0 - hz_ext = grid.h[2] - - # Fill-up property_below. - if grid.nodes_z[-1] < interface-0.05: # At least 5 cm. - hz_ext = np.r_[hz_ext, interface-grid.nodes_z[-1]] - nzadd += 1 - - # Add 100 m of property_above. - if grid.nodes_z[-1] <= interface+0.001: # +1mm - hz_ext = np.r_[hz_ext, 100] - nzadd += 1 - - if nzadd > 0: - # Extend properties. - property_x = extend_property('property_x', expand, nzadd) - property_y = extend_property('property_y', expand, nzadd) - property_z = extend_property('property_z', expand, nzadd) - mu_r = extend_property('mu_r', [1, 1], nzadd) - epsilon_r = extend_property('epsilon_r', [1, 1], nzadd) - - # Create extended grid and model. - grid = meshes.TensorMesh( - [grid.h[0], grid.h[1], hz_ext], origin=grid.origin) - model = models.Model( - grid, property_x, property_y, property_z, mu_r, - epsilon_r, mapping=model.map.name) - - return model + """Deprecated, moved to models: `emg3d.models.expand_grid_model`.""" + msg = ("emg3d: `expand_grid_model` moved from `simulations` to `models`. " + "Its availability in `simulations` will be removed in v1.4.0.") + warnings.warn(msg, FutureWarning) + return models.expand_grid_model(model, expand, interface) def estimate_gridding_opts(gridding_opts, model, survey, input_sc2=None): - """Estimate parameters for automatic gridding. - - Automatically determines the required gridding options from the provided - model, and survey, if they are not provided in ``gridding_opts``. - - The dict ``gridding_opts`` can contain any input parameter taken by - :func:`emg3d.meshes.construct_mesh`, see the corresponding documentation - for more details with regards to the possibilities. - - Different keys of ``gridding_opts`` are treated differently: - - - The following parameters are estimated from the ``model`` if not - provided: - - - ``properties``: lowest conductivity / highest resistivity in the - outermost layer in a given direction. This is usually air in x/y and - positive z. Note: This is very conservative. If you go into deeper - water you could provide less conservative values. - - ``mapping``: taken from model. - - - The following parameters are estimated from the ``survey`` if not - provided: - - - ``frequency``: average (on log10-scale) of all frequencies. - - ``center``: center of all sources. - - ``domain``: from ``vector`` or ``distance``, if provided, or - - - in x/y-directions: extent of sources and receivers plus 10% on each - side, ensuring ratio of 3. - - in z-direction: extent of sources and receivers, ensuring ratio of 2 - to horizontal dimension; 1/10 tenth up, 9/10 down. - - The ratio means that it is enforced that the survey dimension in x or - y-direction is not smaller than a third of the survey dimension in the - other direction. If not, the smaller dimension is expanded - symmetrically. Similarly in the vertical direction, which must be at - least half the dimension of the maximum horizontal dimension or 5 km, - whatever is smaller. Otherwise it is expanded in a ratio of 9 parts - downwards, one part upwards. - - - The following parameter is taken from the ``grid`` if provided as a - string: - - - ``vector``: This is the only real "difference" to the inputs of - :func:`emg3d.meshes.construct_mesh`. The normal input is accepted, but - it can also be a string containing any combination of ``'x'``, ``'y'``, - and ``'z'``. All directions contained in this string are then taken - from the provided grid. E.g., if ``gridding_opts['vector']='xz'`` it - will take the x- and z-directed vectors from the grid. - - - The following parameters are simply passed along if they are provided, - nothing is done otherwise: - - - ``vector`` - - ``distance`` - - ``stretching`` - - ``seasurface`` - - ``cell_numbers`` - - ``lambda_factor`` - - ``lambda_from_center`` - - ``max_buffer`` - - ``min_width_limits`` - - ``min_width_pps`` - - ``verb`` - - - Parameters - ---------- - gridding_opts : dict - Containing input parameters to provide to - :func:`emg3d.meshes.construct_mesh`. See the corresponding - documentation and the explanations above. - - model : Model - The model; a :class:`emg3d.models.Model` instance. - - survey : Survey - The survey; a :class:`emg3d.surveys.Survey` instance. - - input_sc2 : int, default: None - If :func:`emg3d.simulations.expand_grid_model` was used, ``input_sc2`` - corresponds to the original ``grid.shape_cells[2]``. - - - Returns - ------- - gridding_opts : dict - Dict to provide to :func:`emg3d.meshes.construct_mesh`. - - """ - # Initiate new gridding_opts. - gopts = {} - grid = model.grid - - # Optional values that we only include if provided. - for name in ['seasurface', 'cell_numbers', 'lambda_factor', - 'lambda_from_center', 'max_buffer', 'verb']: - if name in gridding_opts.keys(): - gopts[name] = gridding_opts.pop(name) - for name in ['stretching', 'min_width_limits', 'min_width_pps']: - if name in gridding_opts.keys(): - value = gridding_opts.pop(name) - if isinstance(value, (list, tuple)) and len(value) == 3: - value = {'x': value[0], 'y': value[1], 'z': value[2]} - gopts[name] = value - - # Mapping defaults to model map. - gopts['mapping'] = gridding_opts.pop('mapping', model.map) - if not isinstance(gopts['mapping'], str): - gopts['mapping'] = gopts['mapping'].name - - # Frequency defaults to average frequency (log10). - frequency = 10**np.mean(np.log10([v for v in survey.frequencies.values()])) - gopts['frequency'] = gridding_opts.pop('frequency', frequency) - - # Center defaults to center of all sources. - center = np.array([s.center for s in survey.sources.values()]).mean(0) - gopts['center'] = gridding_opts.pop('center', center) - - # Vector. - vector = gridding_opts.pop('vector', None) - if isinstance(vector, str): - # If vector is a string we take the corresponding vectors from grid. - vector = ( - grid.nodes_x if 'x' in vector.lower() else None, - grid.nodes_y if 'y' in vector.lower() else None, - grid.nodes_z[:input_sc2] if 'z' in vector.lower() else None, - ) - gopts['vector'] = vector - if isinstance(vector, dict): - vector = (vector['x'], vector['y'], vector['z']) - elif vector is not None and len(vector) == 3: - gopts['vector'] = {'x': vector[0], 'y': vector[1], 'z': vector[2]} - - # Distance. - distance = gridding_opts.pop('distance', None) - gopts['distance'] = distance - if isinstance(distance, dict): - distance = (distance['x'], distance['y'], distance['z']) - elif distance is not None and len(distance) == 3: - gopts['distance'] = {'x': distance[0], 'y': distance[1], - 'z': distance[2]} - - # Properties defaults to lowest conductivities (AFTER model expansion). - properties = gridding_opts.pop('properties', None) - if properties is None: - - # Get map (in principle the map in gridding_opts could be different - # from the map in the model). - m = gopts['mapping'] - if isinstance(m, str): - m = getattr(maps, 'Map'+m)() - - # Minimum conductivity of all values (x, y, z). - def get_min(ix, iy, iz): - """Get minimum: very conservative/costly, but avoiding problems.""" - - # Collect all x (y, z) values. - data = np.array([]) - for p in ['x', 'y', 'z']: - prop = getattr(model, 'property_'+p) - if prop is not None: - prop = model.map.backward(prop[ix, iy, iz]) - data = np.r_[data, np.min(prop)] - - # Return minimum conductivity (on mapping). - return m.forward(min(data)) - - # Buffer properties. - xneg = get_min(0, slice(None), slice(None)) - xpos = get_min(-1, slice(None), slice(None)) - yneg = get_min(slice(None), 0, slice(None)) - ypos = get_min(slice(None), -1, slice(None)) - zneg = get_min(slice(None), slice(None), 0) - zpos = get_min(slice(None), slice(None), -1) - - # Source property. - ix = np.argmin(abs(grid.nodes_x - gopts['center'][0])) - iy = np.argmin(abs(grid.nodes_y - gopts['center'][1])) - iz = np.argmin(abs(grid.nodes_z - gopts['center'][2])) - source = get_min(ix, iy, iz) - - properties = [source, xneg, xpos, yneg, ypos, zneg, zpos] - - gopts['properties'] = properties - - # Domain; default taken from survey. - domain = gridding_opts.pop('domain', None) - if isinstance(domain, dict): - domain = (domain['x'], domain['y'], domain['z']) - - def get_dim_diff(i): - """Return ([min, max], dim) of inp. - - Take it from domain if provided, else from vector if provided, else - from survey, adding 10% on each side). - """ - if domain is not None and domain[i] is not None: - # domain is provided. - dim = domain[i] - diff = np.diff(dim)[0] - get_it = False - - elif vector is not None and vector[i] is not None: - # vector is provided. - dim = [np.min(vector[i]), np.max(vector[i])] - diff = np.diff(dim)[0] - get_it = False - - elif distance is not None and distance[i] is not None: - # distance is provided. - dim = None - diff = abs(distance[i][0]) + abs(distance[i][1]) - get_it = False - - else: - # Get it from survey, add 5 % on each side. - inp = np.array([s.center[i] for s in survey.sources.values()]) - for s in survey.sources.values(): - inp = np.r_[inp, [r.center_abs(s)[i] - for r in survey.receivers.values()]] - dim = [min(inp), max(inp)] - diff = np.diff(dim)[0] - dim = [min(inp)-diff/10, max(inp)+diff/10] - diff = np.diff(dim)[0] - get_it = True - - diff = np.where(diff > 1e-9, diff, 1e-9) # Avoid division by 0 later - return dim, diff, get_it - - xdim, xdiff, get_x = get_dim_diff(0) - ydim, ydiff, get_y = get_dim_diff(1) - zdim, zdiff, get_z = get_dim_diff(2) - - # Ensure the ratio xdim:ydim is at most 3. - if get_y and xdiff/ydiff > 3: - diff = round((xdiff/3.0 - ydiff)/2.0) - ydim = [ydim[0]-diff, ydim[1]+diff] - elif get_x and ydiff/xdiff > 3: - diff = round((ydiff/3.0 - xdiff)/2.0) - xdim = [xdim[0]-diff, xdim[1]+diff] - - # Ensure the ratio zdim:horizontal is at most 2. - hdist = min(10000, max(xdiff, ydiff)) - if get_z and hdist/zdiff > 2: - diff = round((hdist/2.0 - zdiff)/10.0) - zdim = [zdim[0]-9*diff, zdim[1]+diff] - - # Collect - gopts['domain'] = {'x': xdim, 'y': ydim, 'z': zdim} - - # Ensure no gridding_opts left. - if gridding_opts: - raise TypeError( - f"Unexpected gridding_opts: {list(gridding_opts.keys())}." - ) - - # Return gridding_opts. - return gopts + """Deprecated, moved to meshes: `emg3d.meshes.estimate_gridding_opts`.""" + msg = ("emg3d: `estimate_gridding_opts` moved from `simulations` to " + "`meshes`. Its availability in `simulations` will be removed in " + "v1.4.0.") + warnings.warn(msg, FutureWarning) + return meshes.estimate_gridding_opts( + gridding_opts, model, survey, input_sc2) diff --git a/tests/test_meshes.py b/tests/test_meshes.py index a12b4486..2d4df6ab 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -8,6 +8,8 @@ import emg3d from emg3d import meshes +from . import helpers + # Import soft dependencies. try: @@ -15,6 +17,11 @@ except ImportError: discretize = None +try: + import xarray +except ImportError: + xarray = None + # Data generated with create_data/regression.py REGRES = emg3d.load(join(dirname(__file__), 'data', 'regression.npz')) @@ -461,3 +468,151 @@ def test_check_mesh(): hx = np.ones(16)*20 grid = meshes.TensorMesh(h=[hx, hx, hx], origin=(0, 0, 0)) meshes.check_mesh(grid) + + +@pytest.mark.skipif(xarray is None, reason="xarray not installed.") +class TestEstimateGriddingOpts(): + if xarray is not None: + # Create a simple survey + sources = emg3d.surveys.txrx_coordinates_to_dict( + emg3d.TxElectricDipole, + (0, [1000, 3000, 5000], -950, 0, 0)) + receivers = emg3d.surveys.txrx_coordinates_to_dict( + emg3d.RxElectricPoint, + (np.arange(11)*500, 2000, -1000, 0, 0)) + frequencies = (0.1, 10.0) + + survey = emg3d.Survey( + sources, receivers, frequencies, noise_floor=1e-15, + relative_error=0.05) + + # Create a simple grid and model + grid = meshes.TensorMesh( + [np.ones(32)*250, np.ones(16)*500, np.ones(16)*500], + np.array([-1250, -1250, -2250])) + model = emg3d.Model(grid, 0.1, np.ones(grid.shape_cells)*10) + model.property_y[5, 8, 3] = 100000 # Cell at source center + + def test_empty_dict(self): + gdict = meshes.estimate_gridding_opts({}, self.model, self.survey) + + # Test deprecation v1.4.0 + with pytest.warns(FutureWarning, match="removed in v1.4.0"): + gdict2 = emg3d.simulations.estimate_gridding_opts( + {}, self.model, self.survey) + assert gdict['mapping'] == gdict2['mapping'] + + assert gdict['frequency'] == 1.0 + assert gdict['mapping'] == self.model.map.name + assert_allclose(gdict['center'], (0, 3000, -950)) + assert_allclose(gdict['domain']['x'], (-500, 5500)) + assert_allclose(gdict['domain']['y'], (600, 5400)) + assert_allclose(gdict['domain']['z'], (-3651, -651)) + assert_allclose(gdict['properties'], [100000, 10, 10, 10, 10, 10, 10]) + + def test_mapping_vector(self): + gridding_opts = { + 'mapping': "LgConductivity", + 'vector': 'xZ', + } + gdict = meshes.estimate_gridding_opts( + gridding_opts, self.model, self.survey) + + assert_allclose( + gdict['properties'], + np.log10(1/np.array([100000, 10, 10, 10, 10, 10, 10])), + atol=1e-15) + assert_allclose(gdict['vector']['x'], self.grid.nodes_x) + assert gdict['vector']['y'] is None + assert_allclose(gdict['vector']['z'], self.grid.nodes_z) + + def test_vector_domain_distance(self): + gridding_opts = { + 'vector': 'Z', + 'domain': (None, [-1000, 1000], None), + 'distance': [[5, 10], None, None], + } + gdict = meshes.estimate_gridding_opts( + gridding_opts, self.model, self.survey) + + assert gdict['vector']['x'] == gdict['vector']['y'] is None + assert_allclose(gdict['vector']['z'], self.model.grid.nodes_z) + + assert gdict['domain']['x'] is None + assert gdict['domain']['y'] == [-1000, 1000] + assert gdict['domain']['z'] == [self.model.grid.nodes_z[0], + self.model.grid.nodes_z[-1]] + assert gdict['distance']['x'] == [5, 10] + assert gdict['distance']['y'] == gdict['distance']['z'] is None + + # As dict + gridding_opts = { + 'vector': 'Z', + 'domain': {'x': None, 'y': [-1000, 1000], 'z': None}, + 'distance': {'x': [5, 10], 'y': None, 'z': None}, + } + gdict = meshes.estimate_gridding_opts( + gridding_opts, self.model, self.survey) + + assert gdict['vector']['x'] == gdict['vector']['y'] is None + assert_allclose(gdict['vector']['z'], self.model.grid.nodes_z) + + assert gdict['domain']['x'] is None + assert gdict['domain']['y'] == [-1000, 1000] + assert gdict['domain']['z'] == [self.model.grid.nodes_z[0], + self.model.grid.nodes_z[-1]] + assert gdict['distance']['x'] == [5, 10] + assert gdict['distance']['y'] == gdict['distance']['z'] is None + + def test_pass_along(self): + gridding_opts = { + 'vector': {'x': None, 'y': 1, 'z': None}, + 'stretching': [1.2, 1.3], + 'seasurface': -500, + 'cell_numbers': [10, 20, 30], + 'lambda_factor': 0.8, + 'max_buffer': 10000, + 'min_width_limits': ([20, 40], [20, 40], [20, 40]), + 'min_width_pps': 4, + 'verb': -1, + } + + gdict = meshes.estimate_gridding_opts( + gridding_opts.copy(), self.model, self.survey) + + # Check that all parameters passed unchanged. + gdict2 = {k: gdict[k] for k, _ in gridding_opts.items()} + # Except the tuple, which should be a dict now + gridding_opts['min_width_limits'] = { + 'x': gridding_opts['min_width_limits'][0], + 'y': gridding_opts['min_width_limits'][1], + 'z': gridding_opts['min_width_limits'][2] + } + assert helpers.compare_dicts(gdict2, gridding_opts) + + def test_factor(self): + sources = emg3d.TxElectricDipole((0, 3000, -950, 0, 0)) + receivers = emg3d.RxElectricPoint((0, 3000, -1000, 0, 0)) + + # Adjusted x-domain. + survey = emg3d.Survey( + self.sources, receivers, self.frequencies, noise_floor=1e-15, + relative_error=0.05) + + gdict = meshes.estimate_gridding_opts({}, self.model, survey) + + assert_allclose(gdict['domain']['x'], (-800, 800)) + + # Adjusted x-domain. + survey = emg3d.Survey( + sources, self.receivers, self.frequencies, noise_floor=1e-15, + relative_error=0.05) + + gdict = meshes.estimate_gridding_opts({}, self.model, survey) + + assert_allclose(gdict['domain']['y'], (1500, 3500)) + + def test_error(self): + with pytest.raises(TypeError, match='Unexpected gridding_opts'): + _ = meshes.estimate_gridding_opts( + {'what': True}, self.model, self.survey) diff --git a/tests/test_models.py b/tests/test_models.py index 2e3b34a6..c1f35f8a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -159,6 +159,8 @@ def test_interpolate(self): epsilon_r = property_x*3.33 model1inp = models.Model(grid, property_x) + same = model1inp.interpolate_to_grid(model1inp.grid) + assert model1inp == same model1out = model1inp.interpolate_to_grid(grid2) assert_allclose(model1out.property_x[0], @@ -412,3 +414,43 @@ def test_dict(self): del mdict['grid'] with pytest.raises(KeyError, match="'grid'"): models.Model.from_dict(mdict) + + +def test_expand_grid_model(): + grid = emg3d.TensorMesh([[4, 2, 2, 4], [2, 2, 2, 2], [1, 1]], (0, 0, 0)) + model = emg3d.Model(grid, 1, np.ones(grid.shape_cells)*2, mu_r=3, + epsilon_r=5) + + o_model = models.expand_grid_model(model, [2, 3], 5) + # Test deprecation v1.4.0 + with pytest.warns(FutureWarning, match="removed in v1.4.0"): + o_model2 = emg3d.simulations.expand_grid_model(model, [2, 3], 5) + assert o_model == o_model2 + + # Grid. + assert_allclose(grid.nodes_z, o_model.grid.nodes_z[:-2]) + assert o_model.grid.nodes_z[-2] == 5 + assert o_model.grid.nodes_z[-1] == 105 + + # Property x (from float). + assert_allclose(o_model.property_x[:, :, :-2], 1) + assert_allclose(o_model.property_x[:, :, -2], 2) + assert_allclose(o_model.property_x[:, :, -1], 3) + + # Property y (from shape_cells). + assert_allclose(o_model.property_y[:, :, :-2], model.property_y) + assert_allclose(o_model.property_y[:, :, -2], 2) + assert_allclose(o_model.property_y[:, :, -1], 3) + + # Property z. + assert o_model.property_z is None + + # Property mu_r (from float). + assert_allclose(o_model.mu_r[:, :, :-2], 3) + assert_allclose(o_model.mu_r[:, :, -2], 1) + assert_allclose(o_model.mu_r[:, :, -1], 1) + + # Property epsilon_r (from float). + assert_allclose(o_model.epsilon_r[:, :, :-2], 5) + assert_allclose(o_model.epsilon_r[:, :, -2], 1) + assert_allclose(o_model.epsilon_r[:, :, -1], 1) diff --git a/tests/test_optimize.py b/tests/test_optimize.py deleted file mode 100644 index 35ea66a2..00000000 --- a/tests/test_optimize.py +++ /dev/null @@ -1,201 +0,0 @@ -import pytest -import numpy as np -from numpy.testing import assert_allclose - -import emg3d -from emg3d import optimize - -from . import alternatives - - -# Soft dependencies -try: - import xarray -except ImportError: - xarray = None - - -@pytest.mark.skipif(xarray is None, reason="xarray not installed.") -def test_misfit(): - data = 1 - syn = 5 - rel_err = 0.05 - sources = emg3d.TxElectricDipole((0, 0, 0, 0, 0)) - receivers = emg3d.RxElectricPoint((5, 0, 0, 0, 0)) - - survey = emg3d.Survey( - sources=sources, receivers=receivers, frequencies=100, - data=np.zeros((1, 1, 1))+data, relative_error=0.05, - ) - - grid = emg3d.TensorMesh([np.ones(10)*2, [2, 2], [2, 2]], (-10, -2, -2)) - model = emg3d.Model(grid, 1) - - simulation = emg3d.Simulation(survey=survey, model=model) - - field = emg3d.Field(grid, dtype=np.float64) - field.field += syn - simulation._dict_efield['TxED-1']['f-1'] = field - simulation.data['synthetic'] = simulation.data['observed']*0 + syn - - misfit = 0.5*((syn-data)/(rel_err*data))**2 - assert_allclose(optimize.misfit(simulation), misfit) - - # Missing noise_floor / std. - survey = emg3d.Survey(sources, receivers, 100) - simulation = emg3d.Simulation(survey=survey, model=model) - with pytest.raises(ValueError, match="Either `noise_floor` or"): - optimize.misfit(simulation) - - -@pytest.mark.skipif(xarray is None, reason="xarray not installed.") -class TestGradient: - - def test_errors(self): - mesh = emg3d.TensorMesh([[2, 2], [2, 2], [2, 2]], origin=(-1, -1, -1)) - survey = emg3d.Survey( - sources=emg3d.TxElectricDipole((-1.5, 0, 0, 0, 0)), - receivers=emg3d.RxElectricPoint((1.5, 0, 0, 0, 0)), - frequencies=1.0, - relative_error=0.01, - ) - sim_inp = {'survey': survey, 'gridding': 'same'} - - # Anisotropic models. - simulation = emg3d.Simulation( - model=emg3d.Model(mesh, 1, 2, 3), **sim_inp) - with pytest.raises(NotImplementedError, match='for isotropic models'): - optimize.gradient(simulation) - - # Model with electric permittivity. - simulation = emg3d.Simulation( - model=emg3d.Model(mesh, epsilon_r=3), **sim_inp) - with pytest.raises(NotImplementedError, match='for el. permittivity'): - optimize.gradient(simulation) - - # Model with magnetic permeability. - simulation = emg3d.Simulation( - model=emg3d.Model(mesh, mu_r=np.ones(mesh.shape_cells)*np.pi), - **sim_inp) - with pytest.raises(NotImplementedError, match='for magn. permeabili'): - optimize.gradient(simulation) - - def test_as_vs_fd_gradient(self, capsys): - # Create a simple mesh. - hx = np.ones(64)*100 - mesh = emg3d.TensorMesh([hx, hx, hx], origin=[0, 0, 0]) - - # Define a simple survey, including 1 el. & 1 magn. receiver - survey = emg3d.Survey( - sources=emg3d.TxElectricDipole((1650, 3200, 3200, 0, 0)), - receivers=[ - emg3d.RxElectricPoint((4750, 3200, 3200, 0, 0)), - emg3d.RxMagneticPoint((4750, 3200, 3200, 90, 0)), - ], - frequencies=1.0, - relative_error=0.01, - ) - - # Background Model - con_init = np.ones(mesh.shape_cells) - - # Target Model 1: One Block - con_true = np.ones(mesh.shape_cells) - con_true[27:37, 27:37, 15:25] = 0.001 - - model_init = emg3d.Model(mesh, con_init, mapping='Conductivity') - model_true = emg3d.Model(mesh, con_true, mapping='Conductivity') - # mesh.plot_3d_slicer(con_true) # For debug / QC, needs discretize - - sim_inp = { - 'survey': survey, - 'solver_opts': {'plain': True, 'tol': 5e-5}, # Red. tol 4 speed - 'max_workers': 1, - 'gridding': 'same', - 'verb': 0, - 'receiver_interpolation': 'linear', - } - - # Compute data (pre-computed and passed to Survey above) - sim_data = emg3d.Simulation(model=model_true, **sim_inp) - sim_data.compute(observed=True) - - # Compute adjoint state misfit and gradient - sim_data = emg3d.Simulation(model=model_init, **sim_inp) - data_misfit = sim_data.misfit - grad = sim_data.gradient - - # For Debug / QC, needs discretize - # from matplotlib.colors import LogNorm, SymLogNorm - # mesh.plot_3d_slicer( - # grad.ravel('F'), - # pcolor_opts={ - # 'cmap': 'RdBu_r', - # 'norm': SymLogNorm(linthresh=1e-2, base=10, - # vmin=-1e1, vmax=1e1)} - # ) - - # We test a pseudo-random cell from the inline xz slice. - # - # The NRMSD is (should) be below 1 %. However, (a) close to the - # boundary, (b) in regions where the gradient is almost zero, and (c) - # in regions where the gradient changes sign the NRMSD can become - # large. This is mainly due to numerics, our coarse mesh, and the - # reduced tolerance (which we reduced for speed). As such we only - # sample pseudo-random from 200 cells. - ixyz = ([20, 32, 17], [20, 32, 23], [20, 32, 24], [20, 32, 25], - [20, 32, 26], [20, 32, 27], [20, 32, 28], [20, 32, 29], - [20, 32, 30], [20, 32, 31], [20, 32, 32], [20, 32, 33], - [20, 32, 34], [20, 32, 35], [20, 32, 36], [20, 32, 37], - [20, 32, 38], [20, 32, 39], [21, 32, 23], [21, 32, 24], - [21, 32, 25], [21, 32, 26], [21, 32, 27], [21, 32, 28], - [21, 32, 29], [21, 32, 30], [21, 32, 32], [21, 32, 33], - [21, 32, 34], [21, 32, 35], [21, 32, 36], [21, 32, 37], - [21, 32, 38], [21, 32, 39], [22, 32, 16], [22, 32, 23], - [22, 32, 24], [22, 32, 25], [22, 32, 26], [22, 32, 27], - [22, 32, 28], [22, 32, 29], [22, 32, 30], [22, 32, 31], - [22, 32, 32], [22, 32, 33], [22, 32, 34], [22, 32, 35], - [22, 32, 36], [22, 32, 37], [22, 32, 38], [22, 32, 39], - [23, 32, 16], [23, 32, 23], [23, 32, 24], [23, 32, 25], - [23, 32, 26], [23, 32, 27], [23, 32, 28], [23, 32, 31], - [23, 32, 32], [23, 32, 34], [23, 32, 35], [23, 32, 36], - [23, 32, 37], [23, 32, 38], [23, 32, 39], [24, 32, 15], - [24, 32, 22], [24, 32, 23], [24, 32, 24], [24, 32, 25], - [24, 32, 26], [24, 32, 27], [24, 32, 28], [24, 32, 29], - [24, 32, 31], [24, 32, 32], [24, 32, 34], [24, 32, 35], - [24, 32, 37], [24, 32, 38], [24, 32, 39], [25, 32, 15], - [25, 32, 22], [25, 32, 23], [25, 32, 24], [25, 32, 25], - [25, 32, 26], [25, 32, 28], [25, 32, 31], [25, 32, 32], - [25, 32, 34], [25, 32, 35], [25, 32, 37], [25, 32, 38], - [25, 32, 39], [26, 32, 15], [26, 32, 22], [26, 32, 23], - [26, 32, 24], [26, 32, 25], [26, 32, 26], [26, 32, 31], - [26, 32, 32], [26, 32, 35], [26, 32, 37], [26, 32, 38], - [26, 32, 39], [27, 32, 15], [27, 32, 22], [27, 32, 23], - [27, 32, 24], [27, 32, 25], [27, 32, 26], [27, 32, 28], - [27, 32, 29], [27, 32, 31], [27, 32, 32], [27, 32, 35], - [27, 32, 37], [27, 32, 38], [27, 32, 39], [28, 32, 22], - [28, 32, 23], [28, 32, 24], [28, 32, 25], [28, 32, 26], - [28, 32, 31], [28, 32, 32], [28, 32, 37], [28, 32, 38], - [28, 32, 39], [29, 32, 22], [29, 32, 23], [29, 32, 24], - [29, 32, 25], [29, 32, 26], [29, 32, 31], [29, 32, 32], - [29, 32, 38], [29, 32, 39], [30, 32, 23], [30, 32, 24], - [30, 32, 25], [30, 32, 31], [30, 32, 32], [30, 32, 38], - [30, 32, 39], [31, 32, 23], [31, 32, 24], [31, 32, 25], - [31, 32, 31], [31, 32, 32], [31, 32, 39], [32, 32, 23], - [32, 32, 24], [32, 32, 25], [32, 32, 32], [32, 32, 39], - [33, 32, 23], [33, 32, 24], [33, 32, 25], [33, 32, 32], - [33, 32, 39], [34, 32, 23], [34, 32, 24], [34, 32, 25], - [34, 32, 32], [34, 32, 38], [34, 32, 39], [35, 32, 15], - [35, 32, 24], [35, 32, 25], [35, 32, 38], [35, 32, 39], - [36, 32, 15], [36, 32, 24], [36, 32, 25], [36, 32, 38], - [36, 32, 39], [37, 32, 15], [37, 32, 24], [37, 32, 25], - [37, 32, 38], [38, 32, 25], [38, 32, 38], [39, 32, 16], - [39, 32, 25], [39, 32, 26], [39, 32, 37], [40, 32, 16], - [40, 32, 26], [40, 32, 37], [42, 32, 17], [42, 32, 27], - [42, 32, 36], [43, 32, 18], [43, 32, 28], [43, 32, 35]) - - nrmsd = alternatives.fd_vs_as_gradient( - ixyz[np.random.randint(len(ixyz))], - model_init, grad, data_misfit, sim_inp) - - assert nrmsd < 0.3 diff --git a/tests/test_simulations.py b/tests/test_simulations.py index 8bc22dec..a4227508 100644 --- a/tests/test_simulations.py +++ b/tests/test_simulations.py @@ -3,9 +3,9 @@ from numpy.testing import assert_allclose import emg3d -from emg3d import simulations +from emg3d import simulations, optimize -from . import helpers +from . import alternatives # Soft dependencies @@ -259,6 +259,11 @@ def test_input_gradient(self): with pytest.warns(UserWarning, match='Receiver responses were obtain'): grad = simulation.gradient + # Test deprecation v1.4.0 + with pytest.warns(FutureWarning, match="removed in v1.4.0"): + grad2 = optimize.gradient(simulation) + assert_allclose(grad, grad2) + # Ensure the gradient has the shape of the model, not of the input. assert grad.shape == self.model.shape @@ -488,179 +493,193 @@ def test_rel_abs_rec(self): ) -def test_expand_grid_model(): - grid = emg3d.TensorMesh([[4, 2, 2, 4], [2, 2, 2, 2], [1, 1]], (0, 0, 0)) - model = emg3d.Model(grid, 1, np.ones(grid.shape_cells)*2, mu_r=3, - epsilon_r=5) +@pytest.mark.skipif(xarray is None, reason="xarray not installed.") +def test_misfit(): + data = 1 + syn = 5 + rel_err = 0.05 + sources = emg3d.TxElectricDipole((0, 0, 0, 0, 0)) + receivers = emg3d.RxElectricPoint((5, 0, 0, 0, 0)) - o_model = simulations.expand_grid_model(model, [2, 3], 5) + survey = emg3d.Survey( + sources=sources, receivers=receivers, frequencies=100, + data=np.zeros((1, 1, 1))+data, relative_error=0.05, + ) - # Grid. - assert_allclose(grid.nodes_z, o_model.grid.nodes_z[:-2]) - assert o_model.grid.nodes_z[-2] == 5 - assert o_model.grid.nodes_z[-1] == 105 + grid = emg3d.TensorMesh([np.ones(10)*2, [2, 2], [2, 2]], (-10, -2, -2)) + model = emg3d.Model(grid, 1) - # Property x (from float). - assert_allclose(o_model.property_x[:, :, :-2], 1) - assert_allclose(o_model.property_x[:, :, -2], 2) - assert_allclose(o_model.property_x[:, :, -1], 3) + simulation = simulations.Simulation(survey=survey, model=model) - # Property y (from shape_cells). - assert_allclose(o_model.property_y[:, :, :-2], model.property_y) - assert_allclose(o_model.property_y[:, :, -2], 2) - assert_allclose(o_model.property_y[:, :, -1], 3) + field = emg3d.Field(grid, dtype=np.float64) + field.field += syn + simulation._dict_efield['TxED-1']['f-1'] = field + simulation.data['synthetic'] = simulation.data['observed']*0 + syn - # Property z. - assert o_model.property_z is None + misfit = 0.5*((syn-data)/(rel_err*data))**2 + assert_allclose(simulation.misfit, misfit) - # Property mu_r (from float). - assert_allclose(o_model.mu_r[:, :, :-2], 3) - assert_allclose(o_model.mu_r[:, :, -2], 1) - assert_allclose(o_model.mu_r[:, :, -1], 1) + # Test deprecation v1.4.0 + with pytest.warns(FutureWarning, match="removed in v1.4.0"): + misfit2 = optimize.misfit(simulation) + assert_allclose(misfit, misfit2) - # Property epsilon_r (from float). - assert_allclose(o_model.epsilon_r[:, :, :-2], 5) - assert_allclose(o_model.epsilon_r[:, :, -2], 1) - assert_allclose(o_model.epsilon_r[:, :, -1], 1) + # Missing noise_floor / std. + survey = emg3d.Survey(sources, receivers, 100) + simulation = simulations.Simulation(survey=survey, model=model) + with pytest.raises(ValueError, match="Either `noise_floor` or"): + simulation.misfit @pytest.mark.skipif(xarray is None, reason="xarray not installed.") -class TestEstimateGriddingOpts(): - if xarray is not None: - # Create a simple survey - sources = emg3d.surveys.txrx_coordinates_to_dict( - emg3d.TxElectricDipole, - (0, [1000, 3000, 5000], -950, 0, 0)) - receivers = emg3d.surveys.txrx_coordinates_to_dict( - emg3d.RxElectricPoint, - (np.arange(11)*500, 2000, -1000, 0, 0)) - frequencies = (0.1, 10.0) +class TestGradient: + def test_errors(self): + mesh = emg3d.TensorMesh([[2, 2], [2, 2], [2, 2]], origin=(-1, -1, -1)) survey = emg3d.Survey( - sources, receivers, frequencies, noise_floor=1e-15, - relative_error=0.05) + sources=emg3d.TxElectricDipole((-1.5, 0, 0, 0, 0)), + receivers=emg3d.RxElectricPoint((1.5, 0, 0, 0, 0)), + frequencies=1.0, + relative_error=0.01, + ) + sim_inp = {'survey': survey, 'gridding': 'same', + 'receiver_interpolation': 'linear'} - # Create a simple grid and model - grid = emg3d.TensorMesh( - [np.ones(32)*250, np.ones(16)*500, np.ones(16)*500], - np.array([-1250, -1250, -2250])) - model = emg3d.Model(grid, 0.1, np.ones(grid.shape_cells)*10) - model.property_y[5, 8, 3] = 100000 # Cell at source center - - def test_empty_dict(self): - gdict = simulations.estimate_gridding_opts({}, self.model, self.survey) - - assert gdict['frequency'] == 1.0 - assert gdict['mapping'] == self.model.map.name - assert_allclose(gdict['center'], (0, 3000, -950)) - assert_allclose(gdict['domain']['x'], (-500, 5500)) - assert_allclose(gdict['domain']['y'], (600, 5400)) - assert_allclose(gdict['domain']['z'], (-3651, -651)) - assert_allclose(gdict['properties'], [100000, 10, 10, 10, 10, 10, 10]) - - def test_mapping_vector(self): - gridding_opts = { - 'mapping': "LgConductivity", - 'vector': 'xZ', - } - gdict = simulations.estimate_gridding_opts( - gridding_opts, self.model, self.survey) + # Anisotropic models. + simulation = simulations.Simulation( + model=emg3d.Model(mesh, 1, 2, 3), **sim_inp) + with pytest.raises(NotImplementedError, match='for isotropic models'): + simulation.gradient - assert_allclose( - gdict['properties'], - np.log10(1/np.array([100000, 10, 10, 10, 10, 10, 10])), - atol=1e-15) - assert_allclose(gdict['vector']['x'], self.grid.nodes_x) - assert gdict['vector']['y'] is None - assert_allclose(gdict['vector']['z'], self.grid.nodes_z) - - def test_vector_domain_distance(self): - gridding_opts = { - 'vector': 'Z', - 'domain': (None, [-1000, 1000], None), - 'distance': [[5, 10], None, None], - } - gdict = simulations.estimate_gridding_opts( - gridding_opts, self.model, self.survey) - - assert gdict['vector']['x'] == gdict['vector']['y'] is None - assert_allclose(gdict['vector']['z'], self.model.grid.nodes_z) - - assert gdict['domain']['x'] is None - assert gdict['domain']['y'] == [-1000, 1000] - assert gdict['domain']['z'] == [self.model.grid.nodes_z[0], - self.model.grid.nodes_z[-1]] - assert gdict['distance']['x'] == [5, 10] - assert gdict['distance']['y'] == gdict['distance']['z'] is None - - # As dict - gridding_opts = { - 'vector': 'Z', - 'domain': {'x': None, 'y': [-1000, 1000], 'z': None}, - 'distance': {'x': [5, 10], 'y': None, 'z': None}, - } - gdict = simulations.estimate_gridding_opts( - gridding_opts, self.model, self.survey) - - assert gdict['vector']['x'] == gdict['vector']['y'] is None - assert_allclose(gdict['vector']['z'], self.model.grid.nodes_z) - - assert gdict['domain']['x'] is None - assert gdict['domain']['y'] == [-1000, 1000] - assert gdict['domain']['z'] == [self.model.grid.nodes_z[0], - self.model.grid.nodes_z[-1]] - assert gdict['distance']['x'] == [5, 10] - assert gdict['distance']['y'] == gdict['distance']['z'] is None - - def test_pass_along(self): - gridding_opts = { - 'vector': {'x': None, 'y': 1, 'z': None}, - 'stretching': [1.2, 1.3], - 'seasurface': -500, - 'cell_numbers': [10, 20, 30], - 'lambda_factor': 0.8, - 'max_buffer': 10000, - 'min_width_limits': ([20, 40], [20, 40], [20, 40]), - 'min_width_pps': 4, - 'verb': -1, - } - - gdict = simulations.estimate_gridding_opts( - gridding_opts.copy(), self.model, self.survey) - - # Check that all parameters passed unchanged. - gdict2 = {k: gdict[k] for k, _ in gridding_opts.items()} - # Except the tuple, which should be a dict now - gridding_opts['min_width_limits'] = { - 'x': gridding_opts['min_width_limits'][0], - 'y': gridding_opts['min_width_limits'][1], - 'z': gridding_opts['min_width_limits'][2] - } - assert helpers.compare_dicts(gdict2, gridding_opts) + # Model with electric permittivity. + simulation = simulations.Simulation( + model=emg3d.Model(mesh, epsilon_r=3), **sim_inp) + with pytest.raises(NotImplementedError, match='for el. permittivity'): + simulation.gradient - def test_factor(self): - sources = emg3d.TxElectricDipole((0, 3000, -950, 0, 0)) - receivers = emg3d.RxElectricPoint((0, 3000, -1000, 0, 0)) + # Model with magnetic permeability. + simulation = simulations.Simulation( + model=emg3d.Model(mesh, mu_r=np.ones(mesh.shape_cells)*np.pi), + **sim_inp) + with pytest.raises(NotImplementedError, match='for magn. permeabili'): + simulation.gradient - # Adjusted x-domain. - survey = emg3d.Survey( - self.sources, receivers, self.frequencies, noise_floor=1e-15, - relative_error=0.05) + def test_as_vs_fd_gradient(self, capsys): + # Create a simple mesh. + hx = np.ones(64)*100 + mesh = emg3d.TensorMesh([hx, hx, hx], origin=[0, 0, 0]) - gdict = simulations.estimate_gridding_opts({}, self.model, survey) + # Define a simple survey, including 1 el. & 1 magn. receiver + survey = emg3d.Survey( + sources=emg3d.TxElectricDipole((1650, 3200, 3200, 0, 0)), + receivers=[ + emg3d.RxElectricPoint((4750, 3200, 3200, 0, 0)), + emg3d.RxMagneticPoint((4750, 3200, 3200, 90, 0)), + ], + frequencies=1.0, + relative_error=0.01, + ) - assert_allclose(gdict['domain']['x'], (-800, 800)) + # Background Model + con_init = np.ones(mesh.shape_cells) - # Adjusted x-domain. - survey = emg3d.Survey( - sources, self.receivers, self.frequencies, noise_floor=1e-15, - relative_error=0.05) + # Target Model 1: One Block + con_true = np.ones(mesh.shape_cells) + con_true[27:37, 27:37, 15:25] = 0.001 - gdict = simulations.estimate_gridding_opts({}, self.model, survey) + model_init = emg3d.Model(mesh, con_init, mapping='Conductivity') + model_true = emg3d.Model(mesh, con_true, mapping='Conductivity') + # mesh.plot_3d_slicer(con_true) # For debug / QC, needs discretize - assert_allclose(gdict['domain']['y'], (1500, 3500)) + sim_inp = { + 'survey': survey, + 'solver_opts': {'plain': True, 'tol': 5e-5}, # Red. tol 4 speed + 'max_workers': 1, + 'gridding': 'same', + 'verb': 0, + 'receiver_interpolation': 'linear', + } - def test_error(self): - with pytest.raises(TypeError, match='Unexpected gridding_opts'): - _ = simulations.estimate_gridding_opts( - {'what': True}, self.model, self.survey) + # Compute data (pre-computed and passed to Survey above) + sim_data = simulations.Simulation(model=model_true, **sim_inp) + sim_data.compute(observed=True) + + # Compute adjoint state misfit and gradient + sim_data = simulations.Simulation(model=model_init, **sim_inp) + data_misfit = sim_data.misfit + grad = sim_data.gradient + + # For Debug / QC, needs discretize + # from matplotlib.colors import LogNorm, SymLogNorm + # mesh.plot_3d_slicer( + # grad.ravel('F'), + # pcolor_opts={ + # 'cmap': 'RdBu_r', + # 'norm': SymLogNorm(linthresh=1e-2, base=10, + # vmin=-1e1, vmax=1e1)} + # ) + + # We test a pseudo-random cell from the inline xz slice. + # + # The NRMSD is (should) be below 1 %. However, (a) close to the + # boundary, (b) in regions where the gradient is almost zero, and (c) + # in regions where the gradient changes sign the NRMSD can become + # large. This is mainly due to numerics, our coarse mesh, and the + # reduced tolerance (which we reduced for speed). As such we only + # sample pseudo-random from 200 cells. + ixyz = ([20, 32, 17], [20, 32, 23], [20, 32, 24], [20, 32, 25], + [20, 32, 26], [20, 32, 27], [20, 32, 28], [20, 32, 29], + [20, 32, 30], [20, 32, 31], [20, 32, 32], [20, 32, 33], + [20, 32, 34], [20, 32, 35], [20, 32, 36], [20, 32, 37], + [20, 32, 38], [20, 32, 39], [21, 32, 23], [21, 32, 24], + [21, 32, 25], [21, 32, 26], [21, 32, 27], [21, 32, 28], + [21, 32, 29], [21, 32, 30], [21, 32, 32], [21, 32, 33], + [21, 32, 34], [21, 32, 35], [21, 32, 36], [21, 32, 37], + [21, 32, 38], [21, 32, 39], [22, 32, 16], [22, 32, 23], + [22, 32, 24], [22, 32, 25], [22, 32, 26], [22, 32, 27], + [22, 32, 28], [22, 32, 29], [22, 32, 30], [22, 32, 31], + [22, 32, 32], [22, 32, 33], [22, 32, 34], [22, 32, 35], + [22, 32, 36], [22, 32, 37], [22, 32, 38], [22, 32, 39], + [23, 32, 16], [23, 32, 23], [23, 32, 24], [23, 32, 25], + [23, 32, 26], [23, 32, 27], [23, 32, 28], [23, 32, 31], + [23, 32, 32], [23, 32, 34], [23, 32, 35], [23, 32, 36], + [23, 32, 37], [23, 32, 38], [23, 32, 39], [24, 32, 15], + [24, 32, 22], [24, 32, 23], [24, 32, 24], [24, 32, 25], + [24, 32, 26], [24, 32, 27], [24, 32, 28], [24, 32, 29], + [24, 32, 31], [24, 32, 32], [24, 32, 34], [24, 32, 35], + [24, 32, 37], [24, 32, 38], [24, 32, 39], [25, 32, 15], + [25, 32, 22], [25, 32, 23], [25, 32, 24], [25, 32, 25], + [25, 32, 26], [25, 32, 28], [25, 32, 31], [25, 32, 32], + [25, 32, 34], [25, 32, 35], [25, 32, 37], [25, 32, 38], + [25, 32, 39], [26, 32, 15], [26, 32, 22], [26, 32, 23], + [26, 32, 24], [26, 32, 25], [26, 32, 26], [26, 32, 31], + [26, 32, 32], [26, 32, 35], [26, 32, 37], [26, 32, 38], + [26, 32, 39], [27, 32, 15], [27, 32, 22], [27, 32, 23], + [27, 32, 24], [27, 32, 25], [27, 32, 26], [27, 32, 28], + [27, 32, 29], [27, 32, 31], [27, 32, 32], [27, 32, 35], + [27, 32, 37], [27, 32, 38], [27, 32, 39], [28, 32, 22], + [28, 32, 23], [28, 32, 24], [28, 32, 25], [28, 32, 26], + [28, 32, 31], [28, 32, 32], [28, 32, 37], [28, 32, 38], + [28, 32, 39], [29, 32, 22], [29, 32, 23], [29, 32, 24], + [29, 32, 25], [29, 32, 26], [29, 32, 31], [29, 32, 32], + [29, 32, 38], [29, 32, 39], [30, 32, 23], [30, 32, 24], + [30, 32, 25], [30, 32, 31], [30, 32, 32], [30, 32, 38], + [30, 32, 39], [31, 32, 23], [31, 32, 24], [31, 32, 25], + [31, 32, 31], [31, 32, 32], [31, 32, 39], [32, 32, 23], + [32, 32, 24], [32, 32, 25], [32, 32, 32], [32, 32, 39], + [33, 32, 23], [33, 32, 24], [33, 32, 25], [33, 32, 32], + [33, 32, 39], [34, 32, 23], [34, 32, 24], [34, 32, 25], + [34, 32, 32], [34, 32, 38], [34, 32, 39], [35, 32, 15], + [35, 32, 24], [35, 32, 25], [35, 32, 38], [35, 32, 39], + [36, 32, 15], [36, 32, 24], [36, 32, 25], [36, 32, 38], + [36, 32, 39], [37, 32, 15], [37, 32, 24], [37, 32, 25], + [37, 32, 38], [38, 32, 25], [38, 32, 38], [39, 32, 16], + [39, 32, 25], [39, 32, 26], [39, 32, 37], [40, 32, 16], + [40, 32, 26], [40, 32, 37], [42, 32, 17], [42, 32, 27], + [42, 32, 36], [43, 32, 18], [43, 32, 28], [43, 32, 35]) + + nrmsd = alternatives.fd_vs_as_gradient( + ixyz[np.random.randint(len(ixyz))], + model_init, grad, data_misfit, sim_inp) + + assert nrmsd < 0.3