#
# This file is part of postpic.
#
# postpic is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# postpic is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with postpic. If not, see <http://www.gnu.org/licenses/>.
#
# Stephan Kuschel 2017
"""
Particle related functions.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import numpy as np
import collections
from . import _particlestogrid as ptg
from ..helper import PhysicalConstants
import re
particleshapes = ptg.shapes
__all__ = ['histogramdd', 'SpeciesIdentifier']
[docs]def histogramdd(data, **kwargs):
'''
Creates a histogram of the data. This function has the similar signature and return values as
`numpy.histogramdd`.
In addition this function supports the `shape` keyword argument to choose
the particle shape used. If used with `shape=0` the results of this function and the
`numpy.histogramdd` are identical, however, this function is approx. factor 2 or 3 faster.
Parameters
----------
data: sequence of ndarray or ndarray (1D or 2D)
The input (particle) data for the histogram.
* A 1D numpy array (for 1D histogram).
* A sequence providing the data for the different axis, i.e.
`(datax, datay, dataz)` (preferred).
* A (N, D)-array, i.e. `[[x1, y1, z1], [x2, y2, z2]]` -- must be a numpy array!
bins: sequence or int
The number of bins to use for each dimension
range: sequence, optional
A sequence of lower and upper bin edges to be used if the edges are not given
explicitly in bins. Defaults to the minimum and maximum values along each dimension.
weights: 1D numpy array
The weights to be used for each data point
shape: int
possible choices are:
* 0 - use nearest grid point (NGP)
* 1 - use tophat shape of width 1 bin
* 2 - triangular shape (default)
* 3 - spline 3 shape
Returns
-------
H : ndarray
the final histogram
edges : list
A list of D arrays describing the edges for each dimension
'''
kwshape = kwargs.pop('shape', None)
kwshape = 2 if kwshape is None else kwshape # default value
# default value need to be set separately, such that calling the function
# with `shape=None` or the shape argument not given yields the same result.
kwrange = kwargs.pop('range', None)
kwweights = kwargs.pop('weights', None)
kwbins = kwargs.pop('bins', None)
if len(kwargs) > 0:
raise TypeError("got an unexpected keyword argument {}'".format(kwargs))
try:
shape = data.shape
if shape[0] > 3 and len(shape) == 2: # (N, D) array
# data[:,i] will create a view consuming only microseconds
data = [data[:, i] for i in range(shape[1])]
except(AttributeError):
pass
# upcast 1D if length 1 dimensions are omitted
if np.isscalar(data[0]): # [1,2,3]
data = (data, ) # ([1,2,3],)
if len(data) > 3:
raise ValueError('Data with len {:} not supported. Maximum is 3D data.'.format(len(data)))
if isinstance(kwrange, collections.Iterable) and np.isscalar(kwrange[0]):
kwrange = (kwrange, )
if np.isscalar(kwbins):
kwbins = (kwbins, ) * len(data)
if kwbins is None:
binsdefs = {1: [800],
2: [500, 500],
3: [200, 200, 200]}
kwbins = binsdefs[len(data)]
# data is now (datax, datay, dataz)
# make sure each is an ndarray. If it is already, this operation is fast.
data = [np.asarray(d, dtype='float64') for d in data]
if kwweights is not None:
kwweights = np.asarray(kwweights, dtype='float64')
# 1D, 2D, 3D
ranges = [[None, None] for d in data]
for ax, d in enumerate(data):
for i, f in zip([0, 1], [np.min, np.max]):
try:
ranges[ax][i] = kwrange[ax][i]
if ranges[ax][i] is None:
raise TypeError # catch exception and fill value
if not np.isscalar(ranges[ax][i]):
# if value can be accessed it must be a scalar value
raise ValueError('range="{}" not properly formatted.'.format(kwrange))
except(TypeError):
ranges[ax][i] = f(d)
kwrange = ranges
if len(data) == 1: # ([1,2,3],)
kwrange = kwrange[0]
kwbins = kwbins[0]
h, xedges = ptg.histogram(data[0],
range=kwrange, bins=kwbins,
weights=kwweights, shape=kwshape)
return h, (xedges,)
elif len(data) == 2: # [[1,2,3], [4,5,6]]
h, xedges, yedges = ptg.histogram2d(data[0],
data[1],
range=kwrange, bins=kwbins,
weights=kwweights, shape=kwshape)
return h, (xedges, yedges)
elif len(data) == 3: # [[1,2,3], [4,5,6], [7,8,9]]
h, xe, ye, ze = ptg.histogram3d(data[0],
data[1],
data[2],
range=kwrange, bins=kwbins,
weights=kwweights, shape=kwshape)
return h, (xe, ye, ze)
else:
assert False, 'Internal error'
[docs]class SpeciesIdentifier(PhysicalConstants):
'''
This Class provides static methods for deriving particle properties
from species Names. The only reason for this to be a class is that it
can be used as a mixin.
'''
def _specdict(mass, charge, ision):
return dict(mass=mass * PhysicalConstants.me,
charge=charge * PhysicalConstants.qe,
ision=ision)
# if in default, use this
_defaults = {'electrongold': _specdict(1, -1, False),
'proton': _specdict(1836.2 * 1, 1, True),
'Proton': _specdict(1836.2 * 1, 1, True),
'ionp': _specdict(1836.2, 1, True),
'ion': _specdict(1836.2 * 12, 1, True),
'c6': _specdict(1836.2 * 12, 1, True),
'ionf': _specdict(1836.2 * 19, 1, True),
'Palladium': _specdict(1836.2 * 106, 0, True),
'Palladium1': _specdict(1836.2 * 106, 1, True),
'Palladium2': _specdict(1836.2 * 106, 2, True),
'Ion': _specdict(1836.2, 1, True),
'Photon': _specdict(0, 0, False),
'Positron': _specdict(1, 1, False),
'positron': _specdict(1, 1, False),
'gold1': _specdict(1836.2 * 197, 1, True),
'gold3': _specdict(1836.2 * 197, 3, True),
'gold4': _specdict(1836.2 * 197, 4, True),
'gold2': _specdict(1836.2 * 197, 2, True),
'gold7': _specdict(1836.2 * 197, 7, True),
'gold10': _specdict(1836.2 * 197, 10, True),
'gold20': _specdict(1836.2 * 197, 20, True)}
# unit: amu
_masslistelement = {'H': 1, 'He': 4,
'Li': 6.9, 'C': 12, 'N': 14, 'O': 16, 'F': 19, 'Ne': 20.2,
'Na': 23, 'Al': 27, 'Si': 28, 'S': 32, 'Cl': 35.5, 'Ar': 40,
'Ti': 47.9, 'Cr': 52, 'Fe': 55.8, 'Cu': 63.5, 'Zn': 65.4, 'Kr': 83.8,
'Rb': 85.5, 'Zr': 91.2, 'Pd': 106.4, 'Ag': 107.8, 'Sn': 118.7,
'Xe': 131.3,
'W': 183.8, 'Pt': 195, 'Au': 197, 'Hg': 200.6, 'Pb': 207.2}
[docs] @staticmethod
def isejected(species):
s = species.replace('/', '')
r = re.match(r'(ejected_)(.*)', s)
return r is not None
[docs] @classmethod
def ision(cls, species):
return cls.identifyspecies(species)['ision']
[docs] @classmethod
def identifyspecies(cls, species):
"""
Returns a dictionary containing particle informations deduced from
the species name.
The following keys in the dictionary will always be present:
name species name string
mass kg (SI)
charge C (SI)
tracer boolean
ejected boolean
Valid Examples:
Periodic Table symbol + charge state: c6, F2, H1, C6b
ionm#c# defining mass and charge: ionm12c2, ionc20m110
advanced examples:
ejected_tracer_ionc5m20b, ejected_tracer_electronx,
ejected_c6b, tracer_proton, protonb
"""
ret = {'tracer': False, 'ejected': False, 'name': species}
s = species.replace('/', '_')
if s in cls._defaults:
# if species name is found in cls._defaults use it and
# return result.
ret.update(cls._defaults[s])
return ret
# Regex for parsing ion species name.
# See docsting for valid examples
regex = r'(?P<prae>(.*_)*)' \
r'((?P<elem>((?!El)[A-Z][a-z]?))(?P<elem_c>\d*)|' \
r'(?P<ionmc>(ionc(?P<c1>\d+)m(?P<m2>\d+)|ionm(?P<m1>\d+)c(?P<c2>\d+)))' \
r')?' \
r'(?P<plus>(Plus)*)' \
r'(?P<electron>[Ee]le[ck])?' \
r'(?P<suffix>\w*?)$'
r = re.match(regex, s)
if r is None:
raise Exception('Species ' + str(s) +
' does not match regex name pattern: ' +
str(regex))
regexdict = r.groupdict()
# print(regexdict)
# recognize anz prae and add dictionary key
if regexdict['prae']:
for i in regexdict['prae'].split('_'):
key = i.replace('_', '')
if not key == '':
ret[key] = True
# Excluding patterns start here
# 1) Name Element + charge state: C1, C6, F2, F9, Au20, Pb34a
if regexdict['elem']:
ret['mass'] = float(cls._masslistelement[regexdict['elem']]) * \
1836.2 * cls.me
if regexdict['elem_c'] == '':
ret['charge'] = 0
else:
ret['charge'] = float(regexdict['elem_c']) * cls.qe
ret['ision'] = True
# 2) ionmc like
elif regexdict['ionmc']:
if regexdict['c1']:
ret['mass'] = float(regexdict['m2']) * 1836.2 * cls.me
ret['charge'] = float(regexdict['c1']) * cls.qe
ret['ision'] = True
if regexdict['c2']:
ret['mass'] = float(regexdict['m1']) * 1836.2 * cls.me
ret['charge'] = float(regexdict['c2']) * cls.qe
ret['ision'] = True
# charge may be given via plus
if regexdict['plus']:
charge = len(regexdict['plus'])/4 * cls.qe
if ret['charge'] == 0:
ret['charge'] = charge
# Elektron can be appended to any ion name, so overwrite.
if regexdict['electron']:
ret['mass'] = cls.me
ret['charge'] = -1 * cls.qe
ret['ision'] = False
if not (('mass' in ret) and ('charge' in ret) and ('ision' in ret)):
raise Exception('species ' + species + ' not recognized.')
return ret