The fatiando package has been deprecated. Please check out the new tools in the Fatiando a Terra website: www.fatiando.org

Source code for fatiando.gravmag.harvester

"""
3D potential field inversion by planting anomalous densities.

Implements the method of Uieda and Barbosa (2012a) with improvements by
Uieda and Barbosa (2012b).

A "heuristic" inversion for compact 3D geologic bodies. Performs the inversion
by iteratively growing the estimate around user-specified "seeds". Supports
various kinds of data (gravity, gravity tensor).

The inversion is performed by function
:func:`~fatiando.gravmag.harvester.harvest`. The required information, such as
observed data, seeds, and regularization, are passed to the function through
classes :class:`~fatiando.gravmag.harvester.Seed` and
:class:`~fatiando.gravmag.harvester.Potential`,
:class:`~fatiando.gravmag.harvester.Gz`,
:class:`~fatiando.gravmag.harvester.Gxx`, etc.

See the :ref:`Cookbook <cookbook>` for some example applications to synthetic
data.

**Functions**

* :func:`~fatiando.gravmag.harvester.harvest`: Performs the inversion
* :func:`~fatiando.gravmag.harvester.iharvest`: Iterator to step through the
  inversion one accretion at a time
* :func:`~fatiando.gravmag.harvester.sow`: Creates the seeds from a set of
  (x, y, z) points and physical properties
* :func:`~fatiando.gravmag.harvester.loadseeds`: Loads from a JSON file a set
  of (x, y, z) points and physical properties that specify the seeds. Pass
  output to :func:`~fatiando.gravmag.harvester.sow`
* :func:`~fatiando.gravmag.harvester.weights`: Computes data weights based on
  the distance to the seeds

**Data types**

* :class:`~fatiando.gravmag.harvester.Potential`: gravitational potential
* :class:`~fatiando.gravmag.harvester.Gz`: vertical component of gravitational
  acceleration (i.e., gravity anomaly)
* :class:`~fatiando.gravmag.harvester.Gxx`: North-North component of the
  gravity gradient tensor
* :class:`~fatiando.gravmag.harvester.Gxy`: North-East component of the gravity
  gradient tensor
* :class:`~fatiando.gravmag.harvester.Gxz`: North-vertical component of the
  gravity gradient tensor
* :class:`~fatiando.gravmag.harvester.Gyy`: East-East component of the gravity
  gradient tensor
* :class:`~fatiando.gravmag.harvester.Gyz`: East-vertical component of the
  gravity gradient tensor
* :class:`~fatiando.gravmag.harvester.Gzz`: vertical-vertical component of the
  gravity gradient tensor

**References**

Uieda, L., and V. C. F. Barbosa (2012a), Robust 3D gravity gradient inversion
by planting anomalous densities, Geophysics, 77(4), G55-G66,
doi:10.1190/geo2011-0388.1

Uieda, L., and V. C. F. Barbosa (2012b),
Use of the "shape-of-anomaly" data misfit in 3D inversion by planting anomalous
densities, SEG Technical Program Expanded Abstracts, 1-6,
doi:10.1190/segam2012-0383.1

----

"""
import json
import bisect
from math import sqrt

import numpy

from fatiando.gravmag import prism as prism_engine
from fatiando.gravmag import tesseroid as tesseroid_engine
from fatiando import utils
from fatiando.mesher import Prism, Tesseroid


[docs]def loadseeds(fname): """ Load a set of seed locations and physical properties from a file. The output can then be used with the :func:`~fatiando.gravmag.harvester.sow` function. The seed file should be formatted as:: [ [x1, y1, z1, {"density":dens1}], [x2, y2, z2, {"density":dens2, "magnetization":mag2}], [x3, y3, z3, {"magnetization":mag3, "inclination":inc3, "declination":dec3}], ... ] x, y, z are the coordinates of the seed and the dict (``{'density':2670}``) are its physical properties. .. warning:: Must use ``"``, not ``'``, in the physical property names! Each seed can have different kinds of physical properties. If inclination and declination are not given, will use the inc and dec of the inducing field (i.e., no remanent magnetization). The techie among you will recognize that the seed file is in JSON format. Remember: the coordinate system is x->North, y->East, and z->Down Parameters: * fname : str or file Open file object or filename string Returns: * [[x1, y1, z1, props1], [x2, y2, z2, props2], ...] (x, y, z) are the points where the seeds will be placed and *props* is dict with the values of the physical properties of each, seed. Example: >>> from StringIO import StringIO >>> file = StringIO( ... '[[1, 2, 3, {"density":4, "magnetization":5}],' + ... ' [6, 7, 8, {"magnetization":-1}]]') >>> seeds = loadseeds(file) >>> for s in seeds: ... print s [1, 2, 3, {u'magnetization': 5, u'density': 4}] [6, 7, 8, {u'magnetization': -1}] """ openned = False if isinstance(fname, str): fname = open(fname) openned = True seeds = json.load(fname) if openned: fname.close() return seeds
[docs]def sow(locations, mesh): """ Create the seeds given a list of (x,y,z) coordinates and physical properties. Removes seeds that would fall on the same location with overlapping physical properties. Parameters: * locations : list The locations and physical properties of the seeds. Should be a list like:: [ [x1, y1, z1, {"density":dens1}], [x2, y2, z2, {"density":dens2, "magnetization":mag2}], [x3, y3, z3, {"magnetization":mag3, "inclination":inc3, "declination":dec3}], ... ] * mesh : :class:`fatiando.mesher.PrismMesh` The mesh that will be used in the inversion. Returns: * seeds : list of seeds The seeds that can be passed to :func:`~fatiando.gravmag.harvester.harvest` """ seeds = [] if mesh.celltype == Tesseroid: seedtype = TesseroidSeed elif mesh.celltype == Prism: seedtype = PrismSeed for x, y, z, props in locations: index = _find_index((x, y, z), mesh) if index is None: raise ValueError( "Couldn't find seed at location (%g,%g,%g)" % (x, y, z)) # Check for duplicates if index not in (s.i for s in seeds): seeds.append(seedtype(index, (x, y, z), mesh[index], props)) return seeds
def _find_index(point, mesh): """ Find the index of the cell that has point inside it. """ x1, x2, y1, y2, z1, z2 = mesh.bounds nz, ny, nx = mesh.shape xs = mesh.get_xs() ys = mesh.get_ys() zs = mesh.get_zs() x, y, z = point if (x <= x2 and x >= x1 and y <= y2 and y >= y1 and ((z <= z2 and z >= z1 and mesh.zdown) or (z >= z2 and z <= z1 and not mesh.zdown))): if mesh.zdown: # -1 because bisect gives the index z would have. I want to know # what index z comes after k = bisect.bisect_left(zs, z) - 1 else: # If z is not positive downward, zs will not be sorted k = len(zs) - bisect.bisect_left(zs[::-1], z) j = bisect.bisect_left(ys, y) - 1 i = bisect.bisect_left(xs, x) - 1 seed = i + j * nx + k * nx * ny # Check if the cell is not masked (topography) if mesh[seed] is not None: return seed return None
[docs]def harvest(data, seeds, mesh, compactness, threshold, report=False): """ Run the inversion algorithm and produce an estimate physical property distribution (density and/or magnetization). Parameters: * data : list of data (e.g., :class:`~fatiando.gravmag.harvester.Gz`) The data that will be inverted. Data used must match the physical properties given to the seeds (e.g., gravity data requires seeds to have ``'density'`` prop) * seeds : list of :class:`~fatiando.gravmag.harvester.Seed` Lits of seeds used to start the growth process of the inversion. Use :func:`~fatiando.gravmag.harvester.sow` to generate seeds. * mesh : :class:`fatiando.mesher.PrismMesh` The mesh used in the inversion. Will estimate the physical property distribution on this mesh * compactness : float The compactness regularing parameter (i.e., how much should the estimate be consentrated around the seeds). Must be positive. To find a good value for this, start with a small value (like 0.001), run the inversion and increase the value until desired compactness is achieved. * threshold : float Control how much the solution can grow (usually downward). In order for estimate to grow by the accretion of 1 prism, this prism must decrease the data misfit measure by *threshold* decimal percent. Depends on the size of the cells in the *mesh* and the distance from a cell to the observations. Use values between 0.001 and 0.000001. If cells are small and *threshold* is large (0.001), the seeds won't grow. If cells are large and *threshold* is small (0.000001), the seeds will grow too much. * report : True or False If ``True``, also will return a dict as:: report = {'goal': goal_function_value, 'shape-of-anomaly': SOA_function_value, 'misfit': data_misfit_value, 'regularizer': regularizing_function_value, 'accretions': number_of_accretions} Returns: * estimate, predicted_data : a dict and a list *estimate* is a dict like:: {'physical_property':array, ...} *estimate* contains the estimates physical properties. The properties present in *estimate* are the ones given to the seeds. Include the properties in the *mesh* using:: mesh.addprop('density', estimate['density']) This way you can plot the estimate using :mod:`fatiando.vis.myv`. *predicted_data* is a list of numpy arrays with the predicted (model) data. The list is in the same order as *data*. To plot a map of the fit for visual inspection and a histogram of the residuals:: from fatiando.vis import mpl mpl.figure() # Plot the observed and predicted data as contours for visual # inspection mpl.subplot(1, 2, 1) mpl.axis('scaled') mpl.title('Observed and predicted data') levels = mpl.contourf(x, y, gz, (ny, nx), 10) mpl.colorbar() # Assuming gz is the only data used mpl.contour(x, y, predicted[0], (ny, nx), levels) # Plot a histogram of the residuals residuals = gz - predicted[0] mpl.subplot(1, 2, 2) mpl.title('Residuals') mpl.hist(residuals, bins=10) mpl.show() # It's also good to see the mean and standard deviation of the # residuals print "Residuals mean:", residuals.mean() print "Residuals stddev:", residuals.std() """ for accretions, update in enumerate(iharvest(data, seeds, mesh, compactness, threshold)): continue estimate, predicted = update[:2] output = [fmt_estimate(estimate, mesh.size), predicted] if report: goal, misfit, regul = update[4:] soa = goal - compactness * 1. / (sum(mesh.shape) / 3.) * regul output.append({'goal': goal, 'misfit': misfit, 'regularizer': regul, 'accretions': accretions, 'shape-of-anomaly': soa}) return output
[docs]def iharvest(data, seeds, mesh, compactness, threshold): """ Same as the :func:`fatiando.gravmag.harvester.harvest` function but this one returns an iterator that yields the information of each accretion. Yields: * [estimate, predicted, new, neighbors, goal, misfit, regularizer] The unformated estimate, predicted data vectors, the new element added during this iteration, list of neighbors, goal function value, misfit, regularizing function value. The first yield contains the seeds. Thus ``new`` will be ``None``. To format the estimate in a way that can be added to a mesh, use function fmt_estimate of this module. """ nseeds = len(seeds) estimate = dict((s.i, s.props) for s in seeds) neighbors = [] for seed in seeds: neighbors.append(_get_neighbors(seed, neighbors, estimate, mesh, data)) predicted = _init_predicted(data, seeds, mesh) totalgoal = _shapefunc(data, predicted) totalmisfit = _misfitfunc(data, predicted) regularizer = 0. # Weight the regularizing function by the mean extent of the mesh mu = compactness * 1. / (sum(mesh.shape) / 3.) yield [estimate, predicted, None, neighbors, totalgoal, totalmisfit, regularizer] accretions = 0 for iteration in xrange(mesh.size - nseeds): grew = False # To check if at least one seed grew (stopping criterion) for s in xrange(nseeds): best, bestgoal, bestmisfit, bestregularizer = _grow( neighbors[s], data, predicted, totalmisfit, mu, regularizer, threshold) if best is not None: if best.i not in estimate: estimate[best.i] = {} estimate[best.i].update(best.props) totalgoal = bestgoal totalmisfit = bestmisfit regularizer = bestregularizer for p, e in zip(predicted, best.effect): p += e neighbors[s].pop(best.i) neighbors[s].update( _get_neighbors(best, neighbors, estimate, mesh, data)) grew = True accretions += 1 yield [estimate, predicted, best, neighbors, totalgoal, totalmisfit, regularizer] del best if not grew: break
def _init_predicted(data, seeds, mesh): """ Make a list with the initial predicted data vectors (effect of seeds) """ predicted = [] for d in data: p = numpy.zeros(len(d.observed), dtype='f') for seed in seeds: p += d.effect(mesh[seed.i], seed.props) predicted.append(p) return predicted
[docs]def fmt_estimate(estimate, size): """ Make a nice dict with the estimated physical properties in separate arrays """ output = {} for i in estimate: props = estimate[i] for p in props: if p not in output: output[p] = utils.SparseList(size) output[p][i] = props[p] return output
def _grow(neighbors, data, predicted, totalmisfit, mu, regularizer, threshold): """ Find the neighbor with smallest goal function that also decreases the misfit """ best = None bestgoal = None bestmisfit = None bestregularizer = None for n in neighbors: pred = [p + e for p, e in zip(predicted, neighbors[n].effect)] misfit = _misfitfunc(data, pred) if (misfit < totalmisfit and float(abs(misfit - totalmisfit)) / totalmisfit >= threshold): reg = regularizer + neighbors[n].distance goal = _shapefunc(data, pred) + mu * reg if bestgoal is None or goal < bestgoal: bestgoal = goal best = neighbors[n] bestmisfit = misfit bestregularizer = reg return best, bestgoal, bestmisfit, bestregularizer def _shapefunc(data, predicted): """ Calculate the total shape of anomaly function between the observed and predicted data. """ result = 0. for d, p in zip(data, predicted): alpha = numpy.sum(d.observed * p) / d.norm ** 2 result += numpy.linalg.norm(alpha * d.observed - p) return result def _misfitfunc(data, predicted): """ Calculate the total data misfit function between the observed and predicted data. """ result = 0. for d, p, in zip(data, predicted): residuals = d.observed - p result += sqrt(numpy.dot(d.weights * residuals, residuals)) / d.norm return result def _get_neighbors(cell, neighborhood, estimate, mesh, data): """ Return a dict with the new neighbors of cell. keys are the index of the neighbors in the mesh. values are the Neighbor objects. """ indexes = [n for n in _neighbor_indexes(cell.i, mesh) if not _is_neighbor(n, cell.props, neighborhood) and not _in_estimate(n, cell.props, estimate)] neighbors = dict( (i, Neighbor( i, cell.props, cell.seed, _distance(i, cell.seed, mesh), _calc_effect(i, cell.props, mesh, data))) for i in indexes) return neighbors def _calc_effect(index, props, mesh, data): """ Calculate the effect of cell mesh[index] with physical properties prop for each data set. """ cell = mesh[index] return [d.effect(cell, props) for d in data] def _distance(n, m, mesh): """ Calculate the distance (in number of cells) between cells n and m in mesh. """ ni, nj, nk = _index2ijk(n, mesh) mi, mj, mk = _index2ijk(m, mesh) return sqrt((ni - mi) ** 2 + (nj - mj) ** 2 + (nk - mk) ** 2) def _index2ijk(index, mesh): """ Transform the index of a cell in mesh to a 3-dimensional (i,j,k) index. """ nz, ny, nx = mesh.shape k = index / (nx * ny) j = (index - k * (nx * ny)) / nx i = (index - k * (nx * ny) - j * nx) return i, j, k def _in_estimate(index, props, estimate): """ Check is index is in estimate with props """ if index in estimate: for p in props: if p in estimate[index]: return True return False def _is_neighbor(index, props, neighborhood): """ Check if index is already in the neighborhood with props """ for neighbors in neighborhood: for n in neighbors: if index == neighbors[n].i: for p in props: if p in neighbors[n].props: return True return False def _neighbor_indexes(n, mesh): """Find the indexes of the neighbors of n""" nz, ny, nx = mesh.shape indexes = [] # The guy above tmp = n - nx * ny if tmp > 0: indexes.append(tmp) # The guy below tmp = n + nx * ny if tmp < mesh.size: indexes.append(tmp) # The guy in front tmp = n + 1 if n % nx < nx - 1: indexes.append(tmp) # The guy in the back tmp = n - 1 if n % nx != 0: indexes.append(tmp) # The guy to the left tmp = n + nx if n % (nx * ny) < nx * (ny - 1): indexes.append(tmp) # The guy to the right tmp = n - nx if n % (nx * ny) >= nx: indexes.append(tmp) # Filter out the ones that do not exist or are masked (topography) return [i for i in indexes if i is not None and mesh[i] is not None]
[docs]class PrismSeed(Prism): """ A seed that is a right rectangular prism. """ def __init__(self, i, location, prism, props): Prism.__init__(self, prism.x1, prism.x2, prism.y1, prism.y2, prism.z1, prism.z2, props=props) self.i = i self.seed = i self.x, self.y, self.z = location
[docs]class TesseroidSeed(Tesseroid): """ A seed that is a tesseroid (spherical prism). """ def __init__(self, i, location, tess, props): Tesseroid.__init__(self, tess.w, tess.e, tess.s, tess.n, tess.top, tess.bottom, props=props) self.i = i self.seed = i self.x, self.y, self.z = location
[docs]class Neighbor(object): """ A neighbor. """ def __init__(self, i, props, seed, distance, effect): self.i = i self.props = props self.seed = seed self.distance = distance self.effect = effect
[docs]def weights(x, y, seeds, influences, decay=2): """ Calculate weights for the data based on the distance to the seeds. Use weights to ignore regions of data outside of the target anomaly. Parameters: * x, y : 1d arrays The x and y coordinates of the observations * seeds : list List of seeds, as returned by :func:`~fatiando.gravmag.harvester.sow` * influences : list of floats The respective diameter of influence for each seed. Observations outside the influence will have very small weights. A recommended value is aproximately the diameter of the anomaly * decay : float The decay factor for the weights. Low decay factor makes the weights spread out more. High decay factor makes the transition from large weights to low weights more abrupt. Returns: * weights : 1d array The calculated weights """ distances = numpy.array([((x - s.x) ** 2 + (y - s.y) ** 2) / influence ** 2 for s, influence in zip(seeds, influences)]) # min along axis=0 gets the smallest value from each column weights = numpy.exp(-(distances.min(axis=0) ** decay)) return weights
[docs]class Data(object): """ A container for some potential field data. Know about its data, observation positions, nature of the mesh, and how to calculate the effect of a single cell. """ def __init__(self, x, y, z, data, weights, meshtype): self.x = x self.y = y self.z = z self.observed = data self.size = len(data) self.norm = numpy.linalg.norm(data) self.meshtype = meshtype if self.meshtype not in ['prism', 'tesseroid']: raise AttributeError("Invalid mesh type '%s'" % (meshtype)) if self.meshtype == 'prism': self.engine = prism_engine if self.meshtype == 'tesseroid': self.engine = tesseroid_engine self.weights = weights
[docs]class Potential(Data): """ A container for data of the gravitational potential. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Data.__init__(self, x, y, z, data, weights, meshtype) self.prop = 'density' self.effectfunc = self.engine.potential def effect(self, prism, props): if self.prop not in props: return numpy.zeros(self.size, dtype='f') return self.effectfunc(self.x, self.y, self.z, [prism], props[self.prop])
[docs]class Gz(Potential): """ A container for data of the gravity anomaly. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gz
[docs]class Gxx(Potential): """ A container for data of the xx (north-north) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gxx
[docs]class Gxy(Potential): """ A container for data of the xy (north-east) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gxy
[docs]class Gxz(Potential): """ A container for data of the xz (north-vertical) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gxz
[docs]class Gyy(Potential): """ A container for data of the yy (east-east) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gyy
[docs]class Gyz(Potential): """ A container for data of the yz (east-vertical) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gyz
[docs]class Gzz(Potential): """ A container for data of the zz (vertical-vertical) component of the gravity gradient tensor. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, weights=1., meshtype='prism'): Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.gzz
[docs]class TotalField(Potential): """ A container for data of the total field magnetic anomaly. Coordinate system used: x->North y->East z->Down Parameters: * x, y, z : 1D arrays Arrays with the x, y, z coordinates of the data points * data : 1D array The values of the data at the observation points * inc, dec : floats The inclination and declination of the inducing field * weight : float or array The weight of this data set in the misfit function. Pass an array to give weights to each data points or a float to weight the entire misfit function. See function :func:`~fatiando.gravmag.harvester.weights` """ def __init__(self, x, y, z, data, inc, dec, weights=1., meshtype='prism'): if meshtype != 'prism': raise AttributeError( "Unsupported mesh type '%s' for total field anomaly." % (meshtype)) Potential.__init__(self, x, y, z, data, weights, meshtype) self.effectfunc = self.engine.tf self.prop = 'magnetization' self.inc = inc self.dec = dec def effect(self, prism, props): if self.prop not in props: return numpy.zeros(self.size, dtype='f') return self.effectfunc(self.x, self.y, self.z, [prism], self.inc, self.dec, pmag=props[self.prop])