"""
The actual multigrid solver routines. The computationally intensive parts,
however, are in :mod:`emg3d.core` as numba-jitted functions.
The only relevant function, from an end-user perspective, is
:func:`emg3d.solver.solve`. The other functions and classes are not meant to be
called directly, they are all used by the solver internally. It can, however,
still be insightful to look at the documentation and code of these functions if
you are interested in understanding how the multigrid solver works, the theory
and its implementation.
"""
# Copyright 2018 The emsig community.
#
# This file is part of emg3d.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
import itertools
from typing import Union
from dataclasses import dataclass
import numpy as np
import scipy as sp
from emg3d import core, meshes, models, fields, utils
__all__ = ['solve', 'solve_source', 'multigrid', 'krylov', 'smoothing',
'restriction', 'prolongation', 'residual', 'MGParameters',
'RegularGridProlongator']
# Remove once scipy >= 3.12 is required!
TOL = 'tol' if int(sp.__version__.split('.')[1]) < 12 else 'rtol'
def __dir__():
return __all__
# MAIN USER-FACING FUNCTIONS
[docs]
def solve(model, sfield, sslsolver=True, semicoarsening=True,
linerelaxation=True, verb=0, **kwargs):
r"""Solver for three-dimensional electromagnetic diffusion.
The principal solver of emg3d is using the multigrid method as presented in
[Muld06]_. Multigrid can be used as a standalone solver, or as a
preconditioner for an iterative solver from the
:mod:`scipy.sparse.linalg`-library, e.g.,
:func:`scipy.sparse.linalg.bicgstab`. Alternatively, these Krylov subspace
solvers can also be used without multigrid at all (see the ``cycle`` and
``sslsolver`` parameters).
Implemented are the F-, V-, and W-cycle schemes for multigrid (``cycle``
parameter), and the amount of smoothing steps (initial smoothing,
pre-smoothing, coarsest-grid smoothing, and post-smoothing) can be set
individually (``nu_init``, ``nu_pre``, ``nu_coarse``, and ``nu_post``,
respectively). The maximum level of coarsening can be restricted with the
``clevel`` parameter.
Semicoarsening and line relaxation, as presented in [Muld07]_, are
implemented, see the ``semicoarsening`` and ``linerelaxation`` parameters.
Using the BiCGSTAB solver together with multigrid preconditioning,
semicoarsening, and line relaxation is generally the most robust solution,
albeit not necessarily the fastest. It is the default setting for its
robustness. Just using traditional multigrid without BiCGSTAB nor
semicoarsening nor line relaxation uses the least memory and is often
faster but may fail on stretched grids or for models with strong
anisotropy. However, only testing the parameters for a given model can give
certainty to which parameters are best.
Parameters
----------
model : Model
The model; a :class:`emg3d.models.Model` instance.
sfield : Field
The source field. See :func:`emg3d.fields.get_source_field`.
sslsolver : {str, bool}, default: True
A :mod:`scipy.sparse.linalg`-solver, to use with multigrid as
pre-conditioner or on its own (if ``cycle=None``).
Current possibilities:
- ``False``: Not used, in which case multigrid acts as solver on
its own, not as pre-conditioner.
- ``True`` or ``'bicgstab'``: BiConjugate Gradient STABilized
(:func:`scipy.sparse.linalg.bicgstab`).
- ``'cgs'``: Conjugate Gradient Squared
(:func:`scipy.sparse.linalg.cgs`).
- ``'gcrotmk'``: GCROT: Generalized Conjugate Residual with inner
Orthogonalization and Outer Truncation
(:func:`scipy.sparse.linalg.gcrotmk`).
It does currently not work with ``'cg'``, ``'bicg'``, ``'qmr'``, and
``'minres'`` for various reasons (e.g., some require ``rmatvec`` in
addition to ``matvec``).
semicoarsening : {int, bool}, default: True
Semicoarsening.
- ``True``: Cycling over 1, 2, 3.
- ``0`` or ``False``: No semicoarsening.
- ``1``: Semicoarsening in x direction.
- ``2``: Semicoarsening in y direction.
- ``3``: Semicoarsening in z direction.
- Multi-digit number containing digits from 0 to 3. Multigrid will
cycle over these values, e.g., ``semicoarsening=1213`` will cycle
over [1, 2, 1, 3].
linerelaxation : {int, bool}, default: True
Line relaxation.
- ``True``: Cycling over [4, 5, 6].
- ``0`` or ``False``: No line relaxation.
- ``1``: line relaxation in x direction.
- ``2``: line relaxation in y direction.
- ``3``: line relaxation in z direction.
- ``4``: line relaxation in y and z directions.
- ``5``: line relaxation in x and z directions.
- ``6``: line relaxation in x and y directions.
- ``7``: line relaxation in x, y, and z directions.
- Multi-digit number containing digits from 0 to 7. Multigrid will
cycle over these values, e.g., ``linerelaxation=1213`` will cycle
over [1, 2, 1, 3].
Note: Smoothing is generally done in lexicographical order, except for
line relaxation in y direction; the reason is speed (memory access).
verb : int, default: 0
Level of verbosity (the higher the more verbose).
- ``-1``: Nothing.
- ``0``: Warnings.
- ``1``: One-liner at the end.
- ``2``: One-liner, dynamically updated.
- ``3``: Information about the method plus dynamic one-liner.
- ``4``: Additional information for each MG-cycle.
- ``5``: Everything (slower due to additional error computations).
cycle : {str, None}, default: 'F'
Type of multigrid cycle.
- ``'V'``: V-cycle, simplest version.
- ``'W'``: W-cycle, most expensive version.
- ``'F'``: F-cycle, sort of a compromise between V- and W-cycle.
- ``None``: Does not use multigrid, only the chosen ``sslsolver``.
A ``sslsolver`` has to be provided if ``cycle=None``, and the
``sslsolver`` will then be used without multigrid pre-conditioning.
Comparison of V (left), F (middle), and W (right) cycles for the case
of four grids (three relaxation and prolongation steps)::
h_
2h_ \ / \ / \ /
4h_ \ / \ /\ / \ /\ /
8h_ \/ \/\/ \/ \/\/ \/\/
efield : Field, default: None
Initial electric field. If is initiated with zeroes if not provided.
If an initial efield is provided nothing is returned, but the final
efield is directly put into the provided efield.
If an initial field is provided and a sslsolver is used, then it first
carries out one multigrid cycle without semicoarsening nor line
relaxation. The sslsolver is at times unstable with an initial guess,
carrying out one multigrid cycle helps to stabilize it.
Note that the tangential field at the boundary of a provided efield is
set to zero to ensure a PEC boundary (perfect electric conductor).
tol : float, default: 1e-6
Convergence tolerance.
Iterations stop as soon as the norm of the residual has decreased by
this factor, relative to the residual norm obtained for a zero
electric field.
maxit : int, default: 50
Maximum number of multigrid iterations.
If ``sslsolver`` is used, this applies to the ``sslsolver``.
In the case that multigrid is used as a pre-conditioner for the
``sslsolver``, the maximum iteration for multigrid is defined by the
maximum length of the ``linerelaxation`` and ``semicoarsening``-cycles.
nu_init : int, default: 0
Number of initial smoothing steps, before multigrid cycle.
nu_pre : int, default: 2
Number of pre-smoothing steps.
nu_coarse : int, default: 1
Number of smoothing steps on coarsest grid.
nu_post : int, default: 2
Number of post-smoothing steps.
clevel : int, default: -1
The maximum coarsening level can be different for each dimension and
is, by default, automatically determined (``clevel=-1``). The
parameter ``clevel`` restricts the maximum coarsening level by its
value.
return_info : bool, default: False
If True, a dictionary is returned with runtime info (final norm,
number of iterations of multigrid and the sslsolver, log, exit message,
etc).
log : int, default: 1
Only relevant if ``return_info=True``.
- ``-1``: LOG ONLY: Only store info in log, do not print on screen.
- ``0``: SCREEN only: Only print info to screen, do not store in log.
- ``1``: BOTH: Store info in log and print on screen.
plain : bool, default: False
Plain multigrid method. This is a shortcut for ``sslsolver=False,
semicoarsening=False, linerelaxation=False``. The three parameters
remain unchanged if they are set to anything else than ``True``.
Returns
-------
efield : Field, returned if efield=None
Resulting electric field. It is not returned but stored in-place if an
initial efield was provided.
info_dict : dict, returned if return_info=True
Dictionary with solver info. Keys:
- ``exit``: Exit status, 0=Success, 1=Failure;
- ``exit_message``: Exit message, check this if ``exit=1``;
- ``abs_error``: Absolute error;
- ``rel_error``: Relative error;
- ``ref_error``: Reference error [norm(sfield)];
- ``tol``: Tolerance (abs_error<ref_error*tol);
- ``it_mg``: Number of multigrid iterations;
- ``it_ssl``: Number of SSL iterations;
- ``time``: Runtime (s).
- ``runtime_at_cycle``: Runtime after each cycle (s).
- ``error_at_cycle``: Absolute error after each cycle.
- ``log``: Stored log.
Examples
--------
.. ipython::
In [1]: import emg3d
...: import numpy as np
In [2]: # Create a simple grid, 8 cells of length 1 in each direction,
...: # starting at the origin.
...: hx = np.ones(8)
...: grid = emg3d.TensorMesh([hx, hx, hx], origin=(0, 0, 0))
In [3]: # Create a fullspace model with triaxial anisotropy.
...: model = emg3d.Model(grid, property_x=1.5, property_y=1.8,
...: property_z=3.3, mapping='Resistivity')
In [4]: # The source is a x-directed, horizontal dipole at (4, 4, 4)
...: # with a frequency of 10 Hz.
...: coo = (4, 4, 4, 0, 0) # (x, y, z, azimuth, elevation)
...: sfield = emg3d.fields.get_source_field(
...: grid, source=coo, frequency=10)
In [5]: # Solve for the electric field.
...: efield = emg3d.solve(model, sfield, plain=True, verb=4)
"""
# Undocumented (internal):
# If `always_return=True`, efield is returned even if provided.
always_return = kwargs.pop('always_return', False)
# Extract kwargs which do not go into MGParameters.
if kwargs.pop('plain', False):
sslsolver = False if sslsolver is True else sslsolver
semicoarsening = False if semicoarsening is True else semicoarsening
linerelaxation = False if linerelaxation is True else linerelaxation
efield = kwargs.pop('efield', None)
# Solver settings; get from kwargs or set to default values.
var = MGParameters(
sslsolver=sslsolver, semicoarsening=semicoarsening,
linerelaxation=linerelaxation, shape_cells=model.shape, verb=verb,
**kwargs
)
# Start logging and print all parameters.
var.cprint(f"\n:: emg3d START :: {var.time.now} :: "
f"v{utils.__version__}\n", 2)
var.cprint(var, 2)
# Compute reference error for tolerance.
var.l2_refe = sp.linalg.norm(sfield.field, check_finite=False)
var.error_at_cycle[0] = var.l2_refe
# Check sfield.
if sfield.frequency is None:
raise ValueError(
"Source field is missing frequency information; Create "
"it with `emg3d.fields.get_source_field`, or initiate it "
"with `emg3d.fields.Field`, providing frequency information."
)
# Initiate volume-averaged model values.
vmodel = models.VolumeModel(model, sfield)
# Get efield.
if efield is None:
# If not provided, initiate an empty one.
efield = fields.Field(model.grid, dtype=sfield.field.dtype,
frequency=sfield._frequency)
# Set flag to return the field.
var.do_return = True
else:
# Ensure efield has same data type as sfield.
if sfield.field.dtype != efield.field.dtype:
raise ValueError(
"Source field and electric field must have the same "
"dtype; complex (f-domain) or real (s-domain). Provided:"
f"sfield: {sfield.field.dtype}; efield: {efield.field.dtype}."
)
# If provided efield is missing frequency information, add it from the
# source field.
if efield.frequency is None:
efield._frequency = sfield._frequency
# Ensure PEC.
efield.fx[:, 0, :] = efield.fx[:, -1, :] = 0.
efield.fx[:, :, 0] = efield.fx[:, :, -1] = 0.
efield.fy[0, :, :] = efield.fy[-1, :, :] = 0.
efield.fy[:, :, 0] = efield.fy[:, :, -1] = 0.
efield.fz[0, :, :] = efield.fz[-1, :, :] = 0.
efield.fz[:, 0, :] = efield.fz[:, -1, :] = 0.
# Set flag to NOT return the field (except if always_return was set).
var.do_return = always_return
# If efield is provided, check if it is already sufficiently good.
var.l2 = residual(vmodel, sfield, efield, True)
if var.l2 < var.tol*var.l2_refe:
# Switch-off both sslsolver and multigrid.
var.sslsolver = None
var.cycle = None
# Start final info.
var.exit_message = "CONVERGED"
info = " > NOTHING DONE (provided efield already good enough)\n"
# Check if sfield is zero.
if var.l2_refe < 100*np.finfo(float).tiny:
# To avoid division by zero for the log.
var.l2_refe = np.nan
# Switch-off both sslsolver and multigrid.
var.sslsolver = None
var.cycle = None
# Start final info.
var.exit_message = "CONVERGED"
info = " > RETURN ZERO E-FIELD (provided sfield is zero)\n"
# Zero-source means zero e-field.
efield = fields.Field(model.grid, dtype=sfield.field.dtype,
frequency=sfield._frequency)
# Print header for iteration log.
header = f" [hh:mm:ss] {'rel. error':<22}"
if var.sslsolver:
header += f"{'solver':<20}"
if var.cycle:
header += f"{'MG':<11} l s"
var.cprint(header+"\n", 3)
elif var.cycle:
var.cprint(header+f"{'[abs. error, last/prev]':>29} l s\n", 3)
# Solve the system with...
if var.sslsolver: # ... sslsolver.
krylov(vmodel, sfield, efield, var)
elif var.cycle: # ... multigrid.
multigrid(vmodel, sfield, efield, var)
# Get exit status.
exit_status = int(var.exit_message != 'CONVERGED')
# Print runtime information.
if var.verb in [1, 2]:
_print_one_liner(var, var.l2, True)
elif var.verb > 2:
if var.sslsolver: # sslsolver-specific info.
info = f" > Solver steps : {var.ssl_it}\n"
if var.cycle:
info += f" > MG prec. steps : {var.it}\n"
elif var.cycle: # multigrid-specific info.
info = f" > MG cycles : {var.it}\n"
info += f" > Final rel. error : {var.l2/var.l2_refe:.3e}\n\n"
info += f":: emg3d END :: {var.time.now} :: "
info += f"runtime = {var.time.runtime}\n"
var.cprint(info, 2)
elif var.verb == 0 and exit_status == 1:
var.cprint(f"* WARNING :: {var.exit_message}", -1)
# Assemble the info_dict if return_info
if var.return_info:
info_dict = {
'exit': exit_status, # Exit status.
'exit_message': var.exit_message, # Exit message.
'abs_error': var.l2, # Absolute error.
'rel_error': var.l2/var.l2_refe, # Relative error.
'ref_error': var.l2_refe, # Reference error [norm(sfield)].
'tol': var.tol, # Tolerance (abs_error<ref_error*tol).
'it_mg': var.it, # Multigrid iterations.
'it_ssl': var.ssl_it, # SSL iterations.
'time': var.runtime_at_cycle[-1], # Runtime (s).
'runtime_at_cycle': var.runtime_at_cycle, # Runtime at cycle (s).
'error_at_cycle': var.error_at_cycle, # Abs. error at cycle.
'log': var.log_message, # Log.
}
# Return depending on input arguments; or nothing.
if var.do_return and var.return_info: # efield and info.
return efield, info_dict
elif var.do_return: # efield.
return efield
elif var.return_info: # info.
return info_dict
[docs]
def solve_source(model, source, frequency, **kwargs):
"""Return electric field for a given source and frequency.
This function is a simple shortcut for the following::
sfield = emg3d.get_source_field(grid, source, frequency, **kwargs)
efield = emg3d.solve(model, sfield, **kwargs)
See the documentation of :func:`emg3d.fields.get_source_field` for the
description of ``model``, ``source``, and ``frequency``, and
the documentation of :func:`emg3d.solver.solve` for all other
input and output parameters.
"""
sfield = fields.get_source_field(model.grid, source, frequency)
return solve(model, sfield, **kwargs)
# SOLVERS
[docs]
def multigrid(model, sfield, efield, var, **kwargs):
"""Multigrid solver for three-dimensional electromagnetic diffusion.
Multigrid solver as presented in [Muld06]_, including semicoarsening and
line relaxation as presented in [Muld07]_.
- The electric field is stored in-place in ``efield``.
- The number of multigrid cycles is stored in ``var.it``.
- The current error (l2-norm) is stored in ``var.l2``.
- The reference error (l2-norm of ``sfield``) is stored in ``var.l2_refe``.
This function is the "heart" of the multigrid method, cycling through
different grids, restricting and prolonging accordingly the grids, models,
and fields.
This function is called by :func:`emg3d.solver.solve`.
Parameters
----------
model : VolumeModel
Input model; a :class:`emg3d.models.Model` instance.
sfield : Field
The source field; a :class:`emg3d.fields.Field` instance.
efield : Field
The electric field; a :class:`emg3d.fields.Field` instance.
var : MGParameters
A multigrid parameter instance used within
:func:`emg3d.solver.multigrid`.
level, new_cycmax : int, default: 0
Parameters internally used for recursion (do not use):
- ``level``: current coarsening level;
- ``new_cycmax``: new maximum of multigrid cycles, takes care of
V/W/F-cycling.
"""
# Get recursion parameters.
level = kwargs.get('level', 0)
new_cycmax = kwargs.get('new_cycmax', 0)
# Initiate iteration count.
it = 0
# Get cycmax (depends on cycle and on level [as a fct of sc_dir]).
# This defines the V, W, and F-cycle scheme.
if level == var.clevel[var.sc_dir]:
cycmax = 1
elif new_cycmax == 0 or var.cycle != 'F':
cycmax = var.cycmax
else:
cycmax = new_cycmax
cyc = 0 # Initiate cycle count.
# Compute current error (l2-norms).
l2_last = residual(model, sfield, efield, True)
# Initiate the error-array to check for stagnation.
l2_stag = np.ones(var.maxcycle)*l2_last
# Keep track on the levels during the first cycle, for QC.
if var.first_cycle and var.verb > 3:
var.level_all.append(level)
# Print initial call info.
if level == 0:
var.cprint(" it cycmax error", 4)
var.cprint(" level [ dimension ] info\n", 4)
if var.verb > 4: # Cond. as it causes extra comp. time.
_print_gs_info(var, it, level, cycmax, model.grid, l2_last,
"initial error")
# Initial smoothing (nu_init).
if level == 0 and var.nu_init > 0:
# Smooth and re-compute error.
smoothing(model, sfield, efield, var.nu_init, var.lr_dir)
# Print initial smoothing info.
if var.verb > 4: # Cond. as it causes extra comp. time.
norm = residual(model, sfield, efield, True)
_print_gs_info(var, it, level, cycmax, model.grid, norm,
"initial smoothing")
# Start the actual (recursive) multigrid cycle.
while level == 0 or (level > 0 and it < cycmax):
# Store errors for comparisons (previous and previous of same cycle).
l2_prev = l2_last
l2_stag[(it-1) % var.maxcycle] = l2_last
# (A) Coarsest grid, solve system.
if level == var.clevel[var.sc_dir]:
# Note that the coarsest grid depends on semicoarsening (sc_dir).
# If semicoarsening is carried out along the biggest dimension it
# reduces the number of coarsening levels.
# Gauss-Seidel on the coarsest grid.
smoothing(model, sfield, efield, var.nu_coarse, var.lr_dir)
# Print coarsest grid smoothing info.
if var.verb > 4: # Cond. as it causes extra comp. time.
norm = residual(model, sfield, efield, True)
_print_gs_info(var, it, level, cycmax, model.grid, norm,
"coarsest level")
# (B) Not yet on coarsest grid.
else:
# (B.1) Pre-smoothing (nu_pre).
if var.nu_pre > 0:
smoothing(model, sfield, efield, var.nu_pre, var.lr_dir)
# Print pre-smoothing info.
if var.verb > 4: # Cond. as it causes extra comp. time.
norm = residual(model, sfield, efield, True)
_print_gs_info(var, it, level, cycmax, model.grid, norm,
"pre-smoothing")
# Get sc_dir for this grid.
sc_dir = _current_sc_dir(var.sc_dir, model.grid)
# (B.2) Restrict grid, model, and fields from fine to coarse grid.
res = residual(model, sfield, efield) # Get residual.
cmodel, csfield, cefield = restriction(model, sfield, res, sc_dir)
# (B.3) Recursive call for coarse-grid correction.
multigrid(cmodel, csfield, cefield, var, level=level+1,
new_cycmax=cycmax-cyc)
# (B.4) Add coarse field residual to fine grid field.
prolongation(efield, cefield, sc_dir)
# Append current prolongation level for QC.
if var.first_cycle and var.verb > 3:
var.level_all.append(level)
# (B.5) Post-smoothing (nu_post).
if var.nu_post > 0:
smoothing(model, sfield, efield, var.nu_post, var.lr_dir)
# Print post-smoothing info.
if var.verb > 4: # Cond. as it causes extra comp. time.
norm = residual(model, sfield, efield, True)
_print_gs_info(var, it, level, cycmax, model.grid, norm,
"post-smoothing")
# Update iterator counts.
it += 1 # Local iterator.
if level == 0: # Global iterator (works also when preconditioner.)
var.it += 1
# End loop depending if we are on the original grid or not.
if level > 0: # Update cyc if on a coarse grid.
cyc += 1
else: # Original grid reached, check termination criteria.
# Get current error (l2-norm).
l2_last = residual(model, sfield, efield, True)
# Print end-of-cycle info.
_print_cycle_info(var, l2_last, l2_prev)
# Adjust semicoarsening and line relaxation if they cycle.
if var.sc_cycle:
var.sc_dir = next(var.sc_cycle)
if var.lr_cycle:
var.lr_dir = next(var.lr_cycle)
# Check if any termination criteria is fulfilled.
if _terminate(var, l2_last, l2_stag[(it-1) % var.maxcycle], it):
break
# Store final error (l2-norm).
var.l2 = l2_last
[docs]
def krylov(model, sfield, efield, var):
"""Krylov subspace solver for three-dimensional electromagnetic diffusion.
Using a Krylov subspace iterative solver (defined in ``var.sslsolver``)
implemented in SciPy with or without multigrid as a pre-conditioner
([Muld06]_).
- The electric field is stored in-place in ``efield``.
- The current error (l2-norm) is stored in ``var.l2``.
- The reference error (l2-norm of ``sfield``) is stored in ``var.l2_refe``.
This function is called by :func:`emg3d.solver.solve`.
Parameters
----------
model : VolumeModel
Input model; a :class:`emg3d.models.Model` instance.
sfield : Field
The source field; a :class:`emg3d.fields.Field` instance.
efield : Field
The electric field; a :class:`emg3d.fields.Field` instance.
var : MGParameters
A multigrid parameter instance used within
:func:`emg3d.solver.multigrid`.
"""
# Get frequency
frequency = sfield._frequency
# Define matrix operation A x as LinearOperator.
def amatvec(efield):
"""Compute A x for solver; residual is b-Ax = source - amatvec."""
# Cast current efield to Field instance.
efield = fields.Field(model.grid, efield)
# Compute A x.
rfield = fields.Field(model.grid, dtype=efield.field.dtype,
frequency=frequency)
core.amat_x(
rfield.fx, rfield.fy, rfield.fz,
efield.fx, efield.fy, efield.fz, model.eta_x, model.eta_y,
model.eta_z, model.zeta,
model.grid.h[0], model.grid.h[1], model.grid.h[2])
# Return Field instance.
return -rfield.field
# Initiate LinearOperator A x.
A = sp.sparse.linalg.LinearOperator(
shape=(sfield.field.size, sfield.field.size),
dtype=sfield.field.dtype, matvec=amatvec)
# Define multigrid pre-conditioner as LinearOperator, if `var.cycle`.
def mg_matvec(sfield):
"""Use multigrid as pre-conditioner."""
# Cast current fields to Field instances.
sfield = fields.Field(model.grid, sfield, frequency=frequency)
efield = fields.Field(model.grid, dtype=sfield.field.dtype,
frequency=frequency)
# Solve for these fields.
multigrid(model, sfield, efield, var)
return efield.field
# Initiate LinearOperator M.
M = None
if var.cycle:
M = sp.sparse.linalg.LinearOperator(
shape=(sfield.field.size, sfield.field.size),
dtype=sfield.field.dtype, matvec=mg_matvec)
# Define callback to keep track of sslsolver-iterations.
def callback(x):
"""Solver iteration count and error (l2-norm)."""
# Update iteration count.
var.ssl_it += 1
# Add current runtime and error to var.
var.runtime_at_cycle = np.r_[var.runtime_at_cycle, var.time.elapsed]
var.l2 = residual(model, sfield, fields.Field(model.grid, x), True)
var.error_at_cycle = np.r_[var.error_at_cycle, var.l2]
# Print error (only if verbose).
if var.verb > 3:
log = f" [{var.time.now}] {var.l2/var.l2_refe:.3e} "
log += f" after {var.ssl_it:3} {var.sslsolver}-cycles"
# For those solvers who run an iteration before the first
# preconditioner run ['gcrotmk'].
if var.ssl_it == 1 and var.it == 0 and var.cycle is not None:
log += "\n"
var.cprint(log, 3)
elif var.verb in [2, 3]:
_print_one_liner(var, var.l2)
# Solve the system with sslsolver.
# The ssl solvers do not abort if the norm diverges or is not finite. We
# therefore throw an exception in `_terminate`, and catch it here.
try:
efield.field, i = getattr(sp.sparse.linalg, var.sslsolver)(
A=A, b=sfield.field, x0=efield.field, **{TOL: var.tol},
maxiter=var.ssl_maxit, atol=1e-30, M=M, callback=callback)
except _ConvergenceError:
i = -1 # Mark it as error; returned field is all zero.
var.exit_message += " (returned field is zero)"
# Convergence-checks for sslsolver.
if var.verb == 3:
pre = 50*" " + "\r"
else:
pre = "\n"
pre += " > "
if i < 0:
if var.exit_message == '':
var.exit_message = f"Error in {var.sslsolver} ({i})"
pre = "\n* ERROR :: "
elif i > 0:
var.exit_message = "MAX. ITERATION REACHED, NOT CONVERGED"
else:
var.exit_message = "CONVERGED"
var.cprint(pre+var.exit_message, 2)
# MULTIGRID SUB-ROUTINES
[docs]
def smoothing(model, sfield, efield, nu, lr_dir):
"""Reducing high-frequency error by smoothing.
Solves the linear equation system :math:`A x = b` iteratively using the
Gauss-Seidel method. This acts as smoother or, on the coarsest grid, as a
direct solver.
This is a simple wrapper for the jitted computation in
:func:`emg3d.core.gauss_seidel`, :func:`emg3d.core.gauss_seidel_x`,
:func:`emg3d.core.gauss_seidel_y`, and :func:`emg3d.core.gauss_seidel_z`
(consult these functions for more details and corresponding theory).
The electric fields are updated in-place.
This function is called by :func:`emg3d.solver.multigrid`.
Parameters
----------
model : VolumeModel
Input model; a :class:`emg3d.models.Model` instance.
sfield : Field
Input source field; a :class:`emg3d.fields.Field` instance.
efield : Field
Input electric field; a :class:`emg3d.fields.Field` instance.
nu : int
Number of Gauss-Seidel steps; odd numbers are forward, even numbers are
reversed. E.g., ``nu=2`` is one symmetric Gauss-Seidel iteration, with
a forward and a backward step.
lr_dir : int
Direction of line relaxation.
"""
# Collect Gauss-Seidel input (same for all routines)
inp = (sfield.fx, sfield.fy, sfield.fz,
model.eta_x, model.eta_y, model.eta_z, model.zeta,
model.grid.h[0], model.grid.h[1], model.grid.h[2],
nu)
# Avoid line relaxation in a direction where there are only two cells.
c_lr_dir = _current_lr_dir(lr_dir, model.grid)
# Compute and store fields (in-place)
if c_lr_dir == 0: # Standard MG
core.gauss_seidel(efield.fx, efield.fy, efield.fz, *inp)
if c_lr_dir in [1, 5, 6, 7]: # Line relaxation in x-direction
core.gauss_seidel_x(efield.fx, efield.fy, efield.fz, *inp)
if c_lr_dir in [2, 4, 6, 7]: # Line relaxation in y-direction
core.gauss_seidel_y(efield.fx, efield.fy, efield.fz, *inp)
if c_lr_dir in [3, 4, 5, 7]: # Line relaxation in z-direction
core.gauss_seidel_z(efield.fx, efield.fy, efield.fz, *inp)
[docs]
def restriction(model, sfield, residual, sc_dir):
"""Downsampling of grid, model, and fields to a coarser grid.
The restriction of the residual is used as source term for the coarse grid.
Corresponds to Equations 8 and 9 and surrounding text in [Muld06]_. In the
case of the restriction of the residual, this function is a wrapper for the
jitted functions :func:`emg3d.core.restrict_weights` and
:func:`emg3d.core.restrict` (consult these functions for more details and
corresponding theory).
This function is called by :func:`emg3d.solver.multigrid`.
Parameters
----------
model : VolumeModel
Input model; a :class:`emg3d.models.Model` instance.
sfield : Field
Input source field; a :class:`emg3d.fields.Field` instance.
sc_dir : int
Direction of semicoarsening.
Returns
-------
cmodel : VolumeModel
Coarse model.
csfield : Field
Coarse source field. Corresponds to restriction of fine-grid residual.
cefield : Field
Coarse electric field, complex zeroes.
"""
# 1. RESTRICT GRID
# We take every second element for the direction(s) of coarsening.
rx, ry, rz = 2, 2, 2
if sc_dir in [1, 5, 6]: # No coarsening in x-direction.
rx = 1
if sc_dir in [2, 4, 6]: # No coarsening in y-direction.
ry = 1
if sc_dir in [3, 4, 5]: # No coarsening in z-direction.
rz = 1
# Compute distances of coarse grid.
ch = [np.diff(model.grid.nodes_x[::rx]),
np.diff(model.grid.nodes_y[::ry]),
np.diff(model.grid.nodes_z[::rz])]
# Create new `TensorMesh` instance for coarse grid
cgrid = meshes.BaseMesh(ch, model.grid.origin)
# 2. RESTRICT MODEL
class VolumeModel:
"""Dummy class to create coarse-grid model."""
def __init__(self, case, grid):
"""Initialize with case."""
self.case = case
self.grid = grid
cmodel = VolumeModel(model.case, cgrid)
cmodel.eta_x = _restrict_model_parameters(model.eta_x, sc_dir)
if model.case in ['HTI', 'triaxial']:
cmodel.eta_y = _restrict_model_parameters(model.eta_y, sc_dir)
else:
cmodel.eta_y = cmodel.eta_x
if model.case in ['VTI', 'triaxial']:
cmodel.eta_z = _restrict_model_parameters(model.eta_z, sc_dir)
else:
cmodel.eta_z = cmodel.eta_x
cmodel.zeta = _restrict_model_parameters(model.zeta, sc_dir)
# 3. RESTRICT FIELDS
# Get the weights (Equation 9 of [Muld06]_).
wx, wy, wz = _get_restriction_weights(model.grid, cmodel.grid, sc_dir)
# Compute the source terms (Equation 8 in [Muld06]_).
# Initiate zero field.
csfield = fields.Field(cgrid, dtype=sfield.field.dtype,
frequency=sfield._frequency)
core.restrict(csfield.fx, csfield.fy, csfield.fz, residual.fx,
residual.fy, residual.fz, wx, wy, wz, sc_dir)
# Initiate empty e-field.
cefield = fields.Field(cgrid, dtype=sfield.field.dtype,
frequency=sfield._frequency)
return cmodel, csfield, cefield
[docs]
def prolongation(efield, cefield, sc_dir):
"""Interpolating the electric field from coarse grid to fine grid.
The prolongation from a coarser to a finer grid is the inverse process of
the restriction (:func:`emg3d.solver.restriction`) from a finer to a
coarser grid. The interpolated values of the coarse grid electric field are
added to the fine grid electric field, in-place. Piecewise constant
interpolation is used in the direction of the field, and bilinear
interpolation in the other two directions. See Equation 10 in [Muld06]_ and
surrounding text. Perfect Electric Conductor (PEC) boundary condition is
enforced in this step.
This function is called by :func:`emg3d.solver.multigrid`.
Parameters
----------
efield, cefield : Field
Fine and coarse grid electric fields, :class:`emg3d.fields.Field`
instances.
sc_dir : int
Direction of semicoarsening.
"""
cgrid, grid = cefield.grid, efield.grid
# Interpolate ex in y-z-slices.
fn = RegularGridProlongator(
cgrid.nodes_y, cgrid.nodes_z, grid.nodes_y, grid.nodes_z)
for ixc in range(cgrid.shape_cells[0]):
# Bilinear interpolation in the y-z plane
hh = fn(cefield.fx[ixc, :, :]).reshape(
(grid.shape_nodes[1], grid.shape_nodes[2]), order='F')
# Piecewise constant interpolation in x-direction, ensuring PEC.
if sc_dir not in [1, 5, 6]:
efield.fx[2*ixc, 1:-1, 1:-1] += hh[1:-1, 1:-1]
efield.fx[2*ixc+1, 1:-1, 1:-1] += hh[1:-1, 1:-1]
else:
efield.fx[ixc, 1:-1, 1:-1] += hh[1:-1, 1:-1]
# Interpolate ey in x-z-slices.
fn = RegularGridProlongator(
cgrid.nodes_x, cgrid.nodes_z, grid.nodes_x, grid.nodes_z)
for iyc in range(cgrid.shape_cells[1]):
# Bilinear interpolation in the x-z plane
hh = fn(cefield.fy[:, iyc, :]).reshape(
(grid.shape_nodes[0], grid.shape_nodes[2]), order='F')
# Piecewise constant interpolation in y-direction, ensuring PEC.
if sc_dir not in [2, 4, 6]:
efield.fy[1:-1, 2*iyc, 1:-1] += hh[1:-1, 1:-1]
efield.fy[1:-1, 2*iyc+1, 1:-1] += hh[1:-1, 1:-1]
else:
efield.fy[1:-1, iyc, 1:-1] += hh[1:-1, 1:-1]
# Interpolate ez in x-y-slices.
fn = RegularGridProlongator(
cgrid.nodes_x, cgrid.nodes_y, grid.nodes_x, grid.nodes_y)
for izc in range(cgrid.shape_cells[2]):
# Bilinear interpolation in the x-y plane
hh = fn(cefield.fz[:, :, izc]).reshape(
(grid.shape_nodes[0], grid.shape_nodes[1]), order='F')
# Piecewise constant interpolation in z-direction, ensuring PEC.
if sc_dir not in [3, 4, 5]:
efield.fz[1:-1, 1:-1, 2*izc] += hh[1:-1, 1:-1]
efield.fz[1:-1, 1:-1, 2*izc+1] += hh[1:-1, 1:-1]
else:
efield.fz[1:-1, 1:-1, izc] += hh[1:-1, 1:-1]
[docs]
def residual(model, sfield, efield, norm=False):
r"""Computing the residual.
Returns the complete residual as given in [Muld06]_, page 636, middle of
the right column. This is a simple wrapper for the jitted computation in
:func:`emg3d.core.amat_x` (consult that function for more details and
corresponding theory).
This function is called by :func:`emg3d.solver.multigrid`.
Parameters
----------
model : VolumeModel
Input model; a :class:`emg3d.models.Model` instance.
sfield : Field
Input source field; a :class:`emg3d.fields.Field` instance.
efield : Field
Input electric field; a :class:`emg3d.fields.Field` instance.
norm : bool, default: False
If True, the error (l2-norm) of the residual is returned, not the
residual.
Returns
-------
residual : Field, returned if norm=False
The residual field; :class:`emg3d.fields.Field` instance.
norm : float, returned if norm=True
The error (l2-norm) of the residual.
"""
# Get residual.
rfield = sfield.copy()
core.amat_x(rfield.fx, rfield.fy, rfield.fz, efield.fx, efield.fy,
efield.fz, model.eta_x, model.eta_y, model.eta_z, model.zeta,
model.grid.h[0], model.grid.h[1], model.grid.h[2])
# Return error if norm.
if norm:
return sp.linalg.norm(rfield.field, check_finite=False)
# Return residual if not norm.
else:
return rfield
# VARIABLE DATACLASS
[docs]
@dataclass
class MGParameters:
"""Multigrid solver settings.
This dataclass is used by the main solver routine to keep track of
settings, errors, which level we currently are in, runtime, log, and so on.
This class is instantiated by :func:`emg3d.solver.solve`. Consult that
function for a description of the mandatory and optional input parameters
and more information .
"""
# (A) Mandatory parameters.
# Verbosity.
verb: int
# SciPy Sparse Linalg solver flag.
sslsolver: Union[str, bool]
# Semicoarsening flag.
semicoarsening: Union[int, bool]
# Line relaxation flag.
linerelaxation: Union[int, bool]
# Shape of the input model.
shape_cells: tuple
# (B) Optional parameters with default values
# Type of multigrid cycle.
cycle: Union[str, None] = 'F'
# Convergence tolerance.
tol: float = 1e-6
# Maximum iteration.
maxit: int = 50
# Initial fine-grid smoothing steps before first iteration.
nu_init: int = 0
# Pre-smoothing steps.
nu_pre: int = 2
# Smoothing steps on coarsest grid.
nu_coarse: int = 1
# Post-smoothing steps.
nu_post: int = 2
# Coarsest level; automatically determined if a negative number is given.
clevel: int = -1
# Flag whether or not to return info.
return_info: bool = False
# Log if logging verbosity.
log: int = 0
def __post_init__(self):
"""Set and check some of the parameters."""
# Levels and iterations.
self.level_all = list() # To keep track of the levels for QC-figure.
self.first_cycle = True # Flag if in first cycle for QC-figure.
self.it = 0 # To store multigrid cycle count.
self.ssl_it = 0 # To store solver iteration count.
self.l2 = 1.0 # To store current error.
self.l2_refe = 1.0 # To store reference error.
self._max_level() # Max coarsening levels.
# Initiate logging strings, timer, errors, and return flag.
self.exit_message = '' # For convergence status.
self.log_message = '' # For returning info.
self.time = utils.Timer() # Timer.
self.runtime_at_cycle = np.array([0.]) # Store runtime per cycle.
self.error_at_cycle = np.array([0.]) # Store error per cycle.
self.do_return = True # Whether or not to return the efield.
# Semicoarsening, line relaxation, and Solver/Cycle check.
self._semicoarsening()
self._linerelaxation()
self._solver_and_cycle()
def __repr__(self):
"""Print all relevant parameters."""
outstring = (
f" MG-cycle : {self.cycle!r:17}"
f" sslsolver : {self.sslsolver!r}\n"
#
f" semicoarsening : {self._repr_sc_dir:17}"
f" tol : {self.tol}\n"
#
f" linerelaxation : {self._repr_lr_dir:17}"
f" maxit : {self._repr_maxit}\n"
#
f" nu_{{i,1,c,2}} : {self.nu_init}, {self.nu_pre},"
f" {self.nu_coarse}, {self.nu_post} "
f" verb : {self.verb}\n"
#
f" Original grid : {self.shape_cells[0]:3} x"
f" {self.shape_cells[1]:3} x {self.shape_cells[2]:3} =>"
f" {self.shape_cells[0]*self.shape_cells[1]*self.shape_cells[2]:,}"
f" cells\n"
#
f" Coarsest grid : {self._repr_clevel['shape_cells'][0]:3} x"
f" {self._repr_clevel['shape_cells'][1]:3} x"
f" {self._repr_clevel['shape_cells'][2]:3} "
f" => {self._repr_clevel['n_cells']:,} cells\n"
#
f" Coarsest level : {self._repr_clevel['clevel'][0]:3} ;"
f" {self._repr_clevel['clevel'][1]:3}"
f" ;{self._repr_clevel['clevel'][2]:4} "
f" {self._repr_clevel['message']}\n"
)
return outstring
[docs]
def cprint(self, info, verbosity, **kwargs):
r"""Prints and logs ``info`` if ``self.verb`` > ``verbosity``.
Parameters
----------
info : str
String to be printed.
verbosity : int
Verbosity of info.
kwargs : optional
Arguments passed to ``print()``, e.g., ``end='\r'``.
"""
if self.verb > verbosity:
if self.log != 0:
self.log_message += str(info)+'\n'
if self.log >= 0:
print(info, **kwargs)
def _max_level(self):
r"""Sets dimension-dependent level variable ``clevel``.
Requires at least two cells in each direction.
"""
# Store input clevel for checks.
inp_clevel = np.inf if self.clevel < 0 else self.clevel
# Store maximum division-by-two level for each dimension.
# After that, clevel = [nx, ny, nz], where nx, ny, and nz are the
# number of times you can divide by two in this dimension.
clevel = np.zeros(3, dtype=np.int64)
for i in range(3):
n = self.shape_cells[i]
while n % 2 == 0 and n > 2:
clevel[i] += 1
n /= 2
# Restrict to max coarsening level provided by user.
for i in range(3):
if self.clevel > -1 and self.clevel < clevel[i]:
clevel[i] = self.clevel
# Set overall clevel and store.
self.clevel = np.array(
[max(clevel[0], clevel[1], clevel[2]), # Max-level if sc_dir=0
max(clevel[1], clevel[2]), # Max-level if sc_dir=1
max(clevel[0], clevel[2]), # Max-level if sc_dir=2
max(clevel[0], clevel[1])] # Max-level if sc_dir=3
)
# Store coarsest number of cells on coarsest grid and dimension for the
# log-printing.
sx = int(self.shape_cells[0]/2**clevel[0])
sy = int(self.shape_cells[1]/2**clevel[1])
sz = int(self.shape_cells[2]/2**clevel[2])
self._repr_clevel = {'n_cells': sx*sy*sz, 'shape_cells': (sx, sy, sz),
'clevel': clevel}
# Check some grid characteristics. Good values up to 1024 are:
# - 2*2^{2, ..., 9}: 8, 16, 32, 64, 128, 256, 512, 1024,
# - 3*2^{2, ..., 8}: 12, 24, 48, 96, 192, 384, 768,
# - 5*2^{2, ..., 7}: 20, 40, 80, 160, 320, 640,
# - 7*2^{2, ..., 7}: 28, 56, 112, 224, 448, 896,
# and preference decreases from top to bottom row.
# Check if number on coarsest grid is bigger than 7.
# Ignore if clevel was provided and also reached (user wants it).
check_inp = zip(clevel, [sx, sy, sz])
max_low = any([cl < inp_clevel and sl > 7 for cl, sl in check_inp])
# Check if it can be at least 3 (or inp_clevel) times coarsened.
min_div = any(clevel < min(inp_clevel, 3))
# Raise warning if necessary.
if max_low or min_div:
msg = " :: Grid not optimal for MG solver ::"
self._repr_clevel['message'] = msg
else:
self._repr_clevel['message'] = ""
# Check at least two cells in each direction
if np.any(np.array(self.shape_cells) < 2):
raise ValueError(
"Nr. of cells must be at least two in each direction "
"Provided shape: ({self.shape_cells[0]}, "
f"{self.shape_cells[1]}, {self.shape_cells[2]})."
)
def _semicoarsening(self):
"""Set everything related to semicoarsening."""
# Check input.
if self.semicoarsening is True: # If True, cycle [1, 2, 3].
sc_cycle = np.array([1, 2, 3])
self.sc_cycle = itertools.cycle(sc_cycle)
elif self.semicoarsening in np.arange(4): # If 0-4, use this.
sc_cycle = np.array([int(self.semicoarsening)])
self.sc_cycle = False
else: # Else, use numbers.
sc_cycle = np.array([int(x) for x in
str(abs(self.semicoarsening))])
self.sc_cycle = itertools.cycle(sc_cycle)
# Ensure numbers are within 0 <= sc_dir <= 3
if np.any(sc_cycle < 0) or np.any(sc_cycle > 3):
raise ValueError(
"`semicoarsening` must be one of {False;True;0;1;2;3}. "
"Or a combination of {0;1;2;3} to cycle, e.g. 1213. "
f"Provided: {self.semicoarsening}."
)
# Get first (or only) direction.
if self.sc_cycle:
self.sc_dir = next(self.sc_cycle)
else:
self.sc_dir = sc_cycle[0]
# Set semicoarsening to True/False; print statement
self.semicoarsening = self.sc_dir != 0
self._repr_sc_dir = f"{self.semicoarsening} {sc_cycle}"
self.raw_sc_cycle = sc_cycle
def _linerelaxation(self):
"""Set everything related to line relaxation."""
# Check input.
if self.linerelaxation is True: # If True, cycle [1, 2, 3].
lr_cycle = np.array([4, 5, 6])
self.lr_cycle = itertools.cycle(lr_cycle)
elif self.linerelaxation in np.arange(8): # If 0-7, use this.
lr_cycle = np.array([int(self.linerelaxation)])
self.lr_cycle = False
else: # Else, use numbers.
lr_cycle = np.array([int(x) for x in
str(abs(self.linerelaxation))])
self.lr_cycle = itertools.cycle(lr_cycle)
# Ensure numbers are within 0 <= lr_dir <= 7
if np.any(lr_cycle < 0) or np.any(lr_cycle > 7):
raise ValueError(
"`linerelaxation` must be one of "
"{False;True;0;1;2;3;4;5;6;7}. Or a combination of "
"{1;2;3;4;5;6;7} to cycle, e.g. 1213. "
f"Provided: {self.linerelaxation}."
)
# Get first (only) direction
if self.lr_cycle:
self.lr_dir = next(self.lr_cycle)
else:
self.lr_dir = lr_cycle[0]
# Set linerelaxation to True/False; print statement
self.linerelaxation = self.lr_dir != 0
self._repr_lr_dir = f"{self.linerelaxation} {lr_cycle}"
self.raw_lr_cycle = lr_cycle
def _solver_and_cycle(self):
"""Set everything related to solver and MG-cycle."""
# sslsolver.
solvers = ['bicgstab', 'cgs', 'gcrotmk']
if self.sslsolver is True:
self.sslsolver = 'bicgstab'
elif self.sslsolver is not False and self.sslsolver not in solvers:
raise ValueError(
f"`sslsolver` must be True, False, or one of {solvers}. "
f"Provided: {self.sslsolver!r}."
)
if self.cycle not in ['F', 'V', 'W', None]:
raise ValueError(
"`cycle` must be one of {'F';'V';'W';None}. "
f"Provided: {self.cycle}."
)
# Add maximum multigrid cycles depending on cycle
if self.cycle in ['F', 'W']:
self.cycmax = 2
else:
self.cycmax = 1
# Ensure at least cycle or sslsolver is set
if not self.sslsolver and not self.cycle:
raise ValueError(
"At least `cycle` or `sslsolver` is required. Provided"
f"input: cycle={self.cycle}; sslsolver={self.sslsolver}."
)
# Store maxit in ssl_maxit and adjust maxit if sslsolver.
self.ssl_maxit = 0 # Maximum iteration
self._repr_maxit = f"{self.maxit}" # For printing
self.maxcycle = max(len(self.raw_sc_cycle), len(self.raw_lr_cycle))
if self.sslsolver:
self.ssl_maxit = self.maxit
if self.cycle is not None: # Only if multigrid is used
self.maxit = self.maxcycle
self._repr_maxit += f" ({self.maxit})" # For printing
# INTERPOLATION DATACLASS
[docs]
class RegularGridProlongator:
"""Prolongate field from coarse to fine grid.
This is a heavily modified and adapted version of
:class:`scipy.interpolate.RegularGridInterpolator`.
The main difference (besides the different signature and different
pre-sets) is that this version allows to initiate an instance with the
coarse and fine grids. This initialize will compute the required weights,
and it has therefore only to be done once.
After this, interpolating values from the coarse to the fine grid can be
carried out much faster.
Simplifications in comparison to
:class:`scipy.interpolate.RegularGridInterpolator`:
- No sanity checks;
- Only 2D data;
- ``method='linear'``;
- ``bounds_error=False``;
- ``fill_value=None``.
It results in a speed-up factor of about 2, independent of grid size, for
this particular case.
Parameters
----------
cx, cy, x, y : ndarray
The coordinates defining the coarse (cx, cy) and fine (x, y) grids.
"""
def __init__(self, cx, cy, x, y):
by, bx = np.broadcast_arrays(y, x[:, None])
xy = np.r_[bx.ravel('F'), by.ravel('F')].reshape(-1, 2, order='F')
self.size = xy.shape[0]
self._set_edges_and_weights((cx, cy), xy)
[docs]
def __call__(self, values):
"""Return values of coarse grid on fine grid locations.
Parameters
----------
values : ndarray
Values corresponding to fine-grid (x/y) coordinates.
Returns
-------
result : ndarray
Values corresponding to coarse-grid (cx/cy) coordinates.
"""
# Initiate result.
result = 0.
# Find relevant values.
for n, edge_indices in enumerate(self._get_edges_copy()):
result += np.asarray(values[edge_indices]) * self.weight[n, :]
return result
def _set_edges_and_weights(self, cxy, xy):
"""Compute weights to go from coarse to fine grid coordinates."""
# Find relevant edges between which xy are situated.
indices = []
# Compute distance to lower edge in unity units.
norm_distances = []
# Iterate through dimensions.
for x, grid in zip(xy.T, cxy):
i = np.searchsorted(grid, x) - 1
i[i < 0] = 0
i[i > grid.size - 2] = grid.size - 2
indices.append(i)
norm_distances.append((x - grid[i]) / (grid[i + 1] - grid[i]))
# Find relevant values; each i and i+1 represents a edge.
self.edges = itertools.product(*[[i, i + 1] for i in indices])
# Compute weights.
self.weight = np.ones((4, self.size))
for n, edge_indices in enumerate(self._get_edges_copy()):
partial_weight = 1.
for ei, i, yi in zip(edge_indices, indices, norm_distances):
partial_weight *= np.where(ei == i, 1 - yi, yi)
self.weight[n, :] = partial_weight
def _get_edges_copy(self):
"""Return a copy of the edges-iterator."""
self.edges, edges = itertools.tee(self.edges)
return edges
# MULTIGRID HELPER ROUTINES (private, undocumented)
def _current_sc_dir(sc_dir, grid):
"""Return current direction(s) for semicoarsening.
Checks which directions can actually still be halved and adjusts ``sc_dir``
if necessary for this particular grid.
Parameters
----------
grid : TensorMesh
Current grid; a :class:`emg3d.meshes.TensorMesh` instance.
sc_dir : int
Direction of semicoarsening.
Returns
-------
c_sc_dir : int
Current direction of semicoarsening.
"""
# Find out in which direction we want to half the number of cells.
# This depends on an (optional) direction of semicoarsening, and
# if the number of cells in a direction can still be halved.
xsc_dir = (grid.shape_cells[0] % 2 != 0 or grid.shape_cells[0] < 3
or sc_dir == 1)
ysc_dir = (grid.shape_cells[1] % 2 != 0 or grid.shape_cells[1] < 3
or sc_dir == 2)
zsc_dir = (grid.shape_cells[2] % 2 != 0 or grid.shape_cells[2] < 3
or sc_dir == 3)
# Set current sc_dir depending on the above outcome.
if xsc_dir:
if ysc_dir:
c_sc_dir = 6 # Only coarsen in z-direction.
elif zsc_dir:
c_sc_dir = 5 # Only coarsen in y-direction.
else:
c_sc_dir = 1 # Coarsen in y- and z-directions.
elif ysc_dir:
if zsc_dir:
c_sc_dir = 4 # Only coarsen in x-direction.
else:
c_sc_dir = 2 # Coarsen in x- and z-directions.
elif zsc_dir:
c_sc_dir = 3 # Coarsen in x- and y-directions.
else:
c_sc_dir = 0 # Coarsen in all directions.
return c_sc_dir
def _current_lr_dir(lr_dir, grid):
"""Return current direction(s) for line relaxation.
Checks which directions can actually still apply line relaxation and
adjusts ``lr_dir`` if necessary for this particular grid.
Parameters
----------
grid : TensorMesh
Current grid; a :class:`emg3d.meshes.TensorMesh` instance.
lr_dir : int
Direction of line relaxation.
Returns
-------
c_lr_dir : int
Current direction of line relaxation.
"""
c_lr_dir = np.copy(lr_dir)
if grid.shape_cells[0] == 2: # Check x-direction.
if c_lr_dir == 1:
c_lr_dir = 0
elif c_lr_dir == 5:
c_lr_dir = 3
elif c_lr_dir == 6:
c_lr_dir = 2
elif c_lr_dir == 7:
c_lr_dir = 4
if grid.shape_cells[1] == 2: # Check y-direction.
if c_lr_dir == 2:
c_lr_dir = 0
elif c_lr_dir == 4:
c_lr_dir = 3
elif c_lr_dir == 6:
c_lr_dir = 1
elif c_lr_dir == 7:
c_lr_dir = 5
if grid.shape_cells[2] == 2: # Check z-direction.
if c_lr_dir == 3:
c_lr_dir = 0
elif c_lr_dir == 4:
c_lr_dir = 2
elif c_lr_dir == 5:
c_lr_dir = 1
elif c_lr_dir == 7:
c_lr_dir = 6
return c_lr_dir
def _terminate(var, l2_last, l2_stag, it):
"""Return multigrid termination flag.
Checks all termination criteria and returns True if at least one is
fulfilled.
Parameters
----------
var : MGParameters
A multigrid parameter instance used within
:func:`emg3d.solver.multigrid`.
l2_last, l2_stag : float
Last error and error for stagnation comparison (l2-norms).
it : int
Iteration number.
Returns
-------
finished : bool
Boolean indicating if multigrid is finished.
"""
finished = False
sslabort = False
# Converged if we reached the tolerance.
if l2_last < var.tol*var.l2_refe:
var.exit_message = "CONVERGED"
finished = True
# Diverged if it is 10x larger than last or not a number.
elif l2_last > 10*var.l2_refe or not np.isfinite(l2_last):
var.exit_message = "DIVERGED"
finished = True
sslabort = True # Force abort if sslsolver.
# Stagnated if it is >= the stagnation value.
elif it > 2 and l2_last >= l2_stag:
var.exit_message = "STAGNATED"
finished = True
sslabort = True # Force abort if sslsolver.
# Note: SSL will not fall into this, as it compares to the last value
# of the same cycle type. However, if used as preconditioner each
# cycle-type is only run once, before returning to the SSL.
# Maximum iterations reached.
elif it == var.maxit:
if not var.sslsolver:
var.exit_message = "MAX. ITERATION REACHED, NOT CONVERGED"
finished = True
# Force abort (ssl solver) or print info.
if finished:
# Force abortion of SSL solver.
if var.sslsolver and sslabort:
raise _ConvergenceError
# Print info (if not preconditioner).
elif not var.sslsolver:
if var.verb == 3:
add = 50*" " + "\r"
elif var.verb < 5:
add = "\n"
else:
add = ""
var.cprint(add+" > "+var.exit_message, 2)
return finished
def _restrict_model_parameters(param, sc_dir):
"""Restrict model parameters.
Parameters
----------
param : ndarray
Model parameter to restrict.
sc_dir : int
Direction of semicoarsening.
Returns
-------
out : ndarray
Restricted model parameter.
"""
# Only sum the four cells in y-z-plane
if sc_dir == 1:
out = param[:, :-1:2, :-1:2] + param[:, 1::2, :-1:2]
out += param[:, :-1:2, 1::2] + param[:, 1::2, 1::2]
# Only sum the four cells in x-z-plane
elif sc_dir == 2:
out = param[:-1:2, :, :-1:2] + param[1::2, :, :-1:2]
out += param[:-1:2, :, 1::2] + param[1::2, :, 1::2]
# Only sum the four cells in x-y-plane
elif sc_dir == 3:
out = param[:-1:2, :-1:2, :] + param[1::2, :-1:2, :]
out += param[:-1:2, 1::2, :] + param[1::2, 1::2, :]
# Only sum the two cells in x-direction
elif sc_dir == 4:
out = param[:-1:2, :, :] + param[1::2, :, :]
# Only sum the two cells y-direction
elif sc_dir == 5:
out = param[:, :-1:2, :] + param[:, 1::2, :]
# Only sum the two cells z-direction
elif sc_dir == 6:
out = param[:, :, :-1:2] + param[:, :, 1::2]
# Standard: Sum all 8 cells.
else:
out = param[:-1:2, :-1:2, :-1:2] + param[1::2, :-1:2, :-1:2]
out += param[:-1:2, :-1:2, 1::2] + param[1::2, :-1:2, 1::2]
out += param[:-1:2, 1::2, :-1:2] + param[1::2, 1::2, :-1:2]
out += param[:-1:2, 1::2, 1::2] + param[1::2, 1::2, 1::2]
return out
def _get_restriction_weights(grid, cgrid, sc_dir):
"""Return restriction weights.
Return the weights as given in Equation 9 of [Muld06]_. It is a wrapper for
the numba-jitted function :func:`emg3d.core.restrict_weights`. The
corresponding weights are not actually used in the case of semicoarsening.
We still have to provide arrays of the correct format though, otherwise
numba will complain in the jitted functions.
Parameters
----------
grid, cgrid : TensorMesh
Fine and coarse grids; :class:`emg3d.meshes.TensorMesh` instances.
sc_dir : int
Direction of semicoarsening.
Returns
-------
wx, wy, wz : tuple
Restriction weights in x, y, and z direction, consisting of
(left, center, right) weights.
"""
# x-directed weights.
if sc_dir not in [1, 5, 6]:
wx = core.restrict_weights(
grid.nodes_x, grid.cell_centers_x, grid.h[0], cgrid.nodes_x,
cgrid.cell_centers_x, cgrid.h[0])
else: # Dummy weights in case of semicoarsening.
wxlr = np.zeros(grid.shape_nodes[0], dtype=np.float64)
wx0 = np.ones(grid.shape_nodes[0], dtype=np.float64)
wx = (wxlr, wx0, wxlr)
# y-directed weights.
if sc_dir not in [2, 4, 6]:
wy = core.restrict_weights(
grid.nodes_y, grid.cell_centers_y, grid.h[1], cgrid.nodes_y,
cgrid.cell_centers_y, cgrid.h[1])
else: # Dummy weights in case of semicoarsening.
wylr = np.zeros(grid.shape_nodes[1], dtype=np.float64)
wy0 = np.ones(grid.shape_nodes[1], dtype=np.float64)
wy = (wylr, wy0, wylr)
# z-directed weights.
if sc_dir not in [3, 4, 5]:
wz = core.restrict_weights(
grid.nodes_z, grid.cell_centers_z, grid.h[2], cgrid.nodes_z,
cgrid.cell_centers_z, cgrid.h[2])
else: # Dummy weights in case of semicoarsening.
wzlr = np.zeros(grid.shape_nodes[2], dtype=np.float64)
wz0 = np.ones(grid.shape_nodes[2], dtype=np.float64)
wz = (wzlr, wz0, wzlr)
return wx, wy, wz
class _ConvergenceError(Exception):
"""Custom exception for convergence issues with SSL solvers."""
# VERBOSITY HELPER ROUTINES (private, undocumented)
def _print_cycle_info(var, l2_last, l2_prev):
"""Print cycle info to log.
Parameters
----------
var : MGParameters
A multigrid parameter instance used within
:func:`emg3d.solver.multigrid`.
l2_last, l2_prev : float
Last and previous errors (l2-norms).
"""
# Add current runtime to var.
var.runtime_at_cycle = np.r_[var.runtime_at_cycle, var.time.elapsed]
var.error_at_cycle = np.r_[var.error_at_cycle, l2_last]
# Start info string, return if not enough verbose.
if var.verb in [2, 3]: # One-liner
_print_one_liner(var, l2_last)
if var.verb < 4:
# Set first_cycle to False, to stop logging.
return
elif var.verb > 4:
info = "\n"
else:
info = ""
# Add multigrid-cycle visual QC on first cycle.
if var.first_cycle:
# Cast levels into array, get maximum.
_lvl_all = np.array(var.level_all, dtype=np.int64)
lvl_max = np.max(_lvl_all)
# Get levels, multiply by difference to get +/-.
lvl = (_lvl_all[1:] + _lvl_all[:-1])//2+1
lvl *= _lvl_all[1:] - _lvl_all[:-1]
# Create info string.
out = [" h_\n"]
slen = min(len(lvl), 70)
for cl in range(lvl_max):
out += f" {2**(cl+1):4}h_ "
out += [" " if abs(lvl[v]) != cl+1 else "\\" if
lvl[v] > 0 else "/" for v in range(slen)]
if cl < lvl_max-1:
out.append("\n")
# Add the cycle to inf.
info += "".join(out)
info += "\n\n"
if len(lvl) > 70:
info += " (Cycle-QC restricted to first 70 steps of "
info += f"{len(lvl)} steps.)\n"
# Set first_cycle to False, to reduce verbosity from now on.
var.first_cycle = False
# Add iteration log.
info += f" [{var.time.now}] {l2_last/var.l2_refe:.3e} "
if var.sslsolver: # For multigrid as preconditioner.
info += f"after {19*' '} {var.it:3} {var.cycle}-cycles "
else: # For multigrid as solver.
info += f"after {var.it:3} {var.cycle}-cycles "
info += f"[{l2_last:.3e}, {l2_last/l2_prev:.3f}]"
info += f" {var.lr_dir} {var.sc_dir}"
if var.verb > 4:
info += "\n"
# Print the info.
var.cprint(info, 3)
def _print_gs_info(var, it, level, cycmax, grid, norm, add):
"""Return info-string to log after each Gauss-Seidel smoothing step.
Parameters
----------
it : int
Iteration number.
level : int
Current coarsening level.
cycmax : int
Maximum multigrid cycles.
grid : TensorMesh
Current grid; a :class:`emg3d.meshes.TensorMesh` instance.
norm : float
Current error (l2-norm).
add : str
Information to add at the end.
"""
info = f" {it:2} {level} {cycmax} [{grid.shape_cells[0]:3}, "
info += f"{grid.shape_cells[1]:3}, "
info += f"{grid.shape_cells[2]:3}]: {norm:.3e} "
var.cprint(info + add, 4)
def _print_one_liner(var, l2_last, last=False):
"""Print continuously updated one-liner.
Parameters
----------
l2_last : float
Current error.
last : bool
If True, adds `exit_message` and finishes line.
"""
# Collect info.
info = f":: emg3d :: {l2_last/var.l2_refe:.1e}; " # Absolute error.
if var.sslsolver: # For multigrid as preconditioner.
info += f"{var.ssl_it}({var.it}); "
else: # Stand-alone multigrid.
info += f"{var.it}; "
info += f"{var.time.runtime}" # Runtime
# Print depending on `exit`.
if last:
var.cprint(info+f"; {var.exit_message}", -100)
else:
var.cprint(info, -100, end='\r')