update public distribution
based on internal repository c9a2ac8 2019-01-03 16:04:57 +0100 tagged rev-master-2.0.0
This commit is contained in:
259
pmsco/cluster.py
259
pmsco/cluster.py
@ -14,12 +14,17 @@ pip install --user periodictable
|
||||
|
||||
@author Matthias Muntwiler
|
||||
|
||||
@copyright (c) 2015 by Paul Scherrer Institut
|
||||
@copyright (c) 2015-18 by Paul Scherrer Institut
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import periodictable as pt
|
||||
import sys
|
||||
|
||||
## default file format identifier
|
||||
FMT_DEFAULT = 0
|
||||
@ -30,33 +35,40 @@ FMT_EDAC = 2
|
||||
## XYZ file format identifier
|
||||
FMT_XYZ = 3
|
||||
|
||||
# python version dependent type of chemical symbol
|
||||
if sys.version_info[0] >= 3:
|
||||
_SYMBOL_TYPE = 'U2'
|
||||
else:
|
||||
_SYMBOL_TYPE = 'S2'
|
||||
|
||||
## numpy.array datatype of Cluster.data array
|
||||
DTYPE_CLUSTER_INTERNAL = [('i','i4'), ('t','i4'), ('s','a2'), ('x','f4'), ('y','f4'), ('z','f4'), ('e','u1')]
|
||||
DTYPE_CLUSTER_INTERNAL = [('i', 'i4'), ('t', 'i4'), ('s', _SYMBOL_TYPE), ('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
|
||||
('e', 'u1')]
|
||||
## file format of internal Cluster.data array
|
||||
FMT_CLUSTER_INTERNAL = ["%5u", "%2u", "%s", "%7.3f", "%7.3f", "%7.3f", "%1u"]
|
||||
## field (column) names of internal Cluster.data array
|
||||
FIELDS_CLUSTER_INTERNAL = ['i','t','s','x','y','z','e']
|
||||
FIELDS_CLUSTER_INTERNAL = ['i', 't', 's', 'x', 'y', 'z', 'e']
|
||||
|
||||
## numpy.array datatype of cluster for MSC cluster file input/output
|
||||
DTYPE_CLUSTER_MSC = [('i','i4'), ('x','f4'), ('y','f4'), ('z','f4'), ('t','i4')]
|
||||
DTYPE_CLUSTER_MSC = [('i', 'i4'), ('x', 'f4'), ('y', 'f4'), ('z', 'f4'), ('t', 'i4')]
|
||||
## file format of MSC cluster file
|
||||
FMT_CLUSTER_MSC = ["%5u", "%7.3f", "%7.3f", "%7.3f", "%2u"]
|
||||
## field (column) names of MSC cluster file
|
||||
FIELDS_CLUSTER_MSC = ['i','x','y','z','t']
|
||||
FIELDS_CLUSTER_MSC = ['i', 'x', 'y', 'z', 't']
|
||||
|
||||
## numpy.array datatype of cluster for EDAC cluster file input/output
|
||||
DTYPE_CLUSTER_EDAC= [('i','i4'), ('t','i4'), ('x','f4'), ('y','f4'), ('z','f4')]
|
||||
DTYPE_CLUSTER_EDAC= [('i', 'i4'), ('t', 'i4'), ('x', 'f4'), ('y', 'f4'), ('z', 'f4')]
|
||||
## file format of EDAC cluster file
|
||||
FMT_CLUSTER_EDAC = ["%5u", "%2u", "%7.3f", "%7.3f", "%7.3f"]
|
||||
## field (column) names of EDAC cluster file
|
||||
FIELDS_CLUSTER_EDAC = ['i','t','x','y','z']
|
||||
FIELDS_CLUSTER_EDAC = ['i', 't', 'x', 'y', 'z']
|
||||
|
||||
## numpy.array datatype of cluster for XYZ file input/output
|
||||
DTYPE_CLUSTER_XYZ= [('s','a2'), ('x','f4'), ('y','f4'), ('z','f4')]
|
||||
DTYPE_CLUSTER_XYZ= [('s', _SYMBOL_TYPE), ('x', 'f4'), ('y', 'f4'), ('z', 'f4')]
|
||||
## file format of XYZ cluster file
|
||||
FMT_CLUSTER_XYZ = ["%s", "%10.5f", "%10.5f", "%10.5f"]
|
||||
## field (column) names of XYZ cluster file
|
||||
FIELDS_CLUSTER_XYZ = ['s','x','y','z']
|
||||
FIELDS_CLUSTER_XYZ = ['s', 'x', 'y', 'z']
|
||||
|
||||
|
||||
class Cluster(object):
|
||||
@ -137,7 +149,7 @@ class Cluster(object):
|
||||
"""
|
||||
Copy the data from another cluster.
|
||||
|
||||
@param cluster (Cluster): other Cluster object.
|
||||
@param cluster: (Cluster) other Cluster object.
|
||||
"""
|
||||
self.data = cluster.data.copy()
|
||||
|
||||
@ -164,10 +176,10 @@ class Cluster(object):
|
||||
|
||||
@param x, y, z: (float) atom coordinates in the cluster
|
||||
|
||||
@param emitter: (uint) 1 = emitter, 0 = regular
|
||||
@param emitter: (int or bool) True = emitter, False = scatterer
|
||||
"""
|
||||
symbol = pt.elements[element_number].symbol
|
||||
element = (index, element_number, symbol, x, y, z, emitter)
|
||||
element = (index, element_number, symbol, x, y, z, int(emitter))
|
||||
return element
|
||||
|
||||
def add_atom(self, atomtype, v_pos, is_emitter):
|
||||
@ -178,10 +190,10 @@ class Cluster(object):
|
||||
|
||||
@param v_pos: (numpy.ndarray, shape = (3)) position vector
|
||||
|
||||
@param is_emitter: (uint) 1 = emitter, 0 = regular
|
||||
@param is_emitter: (int or bool) True = emitter, False = scatterer
|
||||
"""
|
||||
n0 = self.data.shape[0] + 1
|
||||
element = self.build_element(n0, atomtype, v_pos[0], v_pos[1], v_pos[2], is_emitter)
|
||||
element = self.build_element(n0, atomtype, v_pos[0], v_pos[1], v_pos[2], int(is_emitter))
|
||||
self.data = np.append(self.data, np.array(element,
|
||||
dtype=self.data.dtype))
|
||||
|
||||
@ -342,6 +354,29 @@ class Cluster(object):
|
||||
|
||||
return idx
|
||||
|
||||
def translate(self, vector, element=0):
|
||||
"""
|
||||
translate the cluster or all atoms of a specified element.
|
||||
|
||||
@param vector: (numpy.ndarray) 3-dimensional displacement vector.
|
||||
@param element: (int) chemical element number if atoms of a specific element should be affected.
|
||||
by default (element = 0), all atoms are moved.
|
||||
@return: (numpy.ndarray) indices of the atoms that have been shifted.
|
||||
"""
|
||||
if element:
|
||||
try:
|
||||
sel = self.data['t'] == int(element)
|
||||
except ValueError:
|
||||
sel = self.data['s'] == element
|
||||
else:
|
||||
sel = np.ones_like(self.data['t'])
|
||||
idx = np.where(sel)
|
||||
self.data['x'][idx] += vector[0]
|
||||
self.data['y'][idx] += vector[1]
|
||||
self.data['z'][idx] += vector[2]
|
||||
|
||||
return idx
|
||||
|
||||
def matrix_transform(self, matrix):
|
||||
"""
|
||||
apply a transformation matrix to each atom of the cluster.
|
||||
@ -474,6 +509,31 @@ class Cluster(object):
|
||||
self.data = self.data[idx]
|
||||
self.update_index()
|
||||
|
||||
def trim_paraboloid(self, rxy, z0):
|
||||
"""
|
||||
remove atoms outside a given paraboloid.
|
||||
|
||||
the paraboloid is defined by z(r) = z0 * (1 - r**2 / rxy**2),
|
||||
where r**2 = x**2 + y**2.
|
||||
its apex is at (0, 0, z0).
|
||||
z(r) = 0 for x**2 + y**2 = rxy**2.
|
||||
|
||||
the coordinates (x,y,z) of atoms to keep must match z >= z(r).
|
||||
|
||||
@param rxy: (float) radius of the paraboloid at z = 0.
|
||||
|
||||
@param z0: (float) vertical coordinate of the apex (at x = y = 0).
|
||||
in the usual cluster layout where the surface is near z = 0 and the atoms at lower z,
|
||||
this value is negative.
|
||||
|
||||
@return: None
|
||||
"""
|
||||
rsq = self.data['x']**2 + self.data['y']**2
|
||||
pz = z0 * (1. - rsq / rxy**2)
|
||||
idx = np.where(self.data['z'] >= pz)
|
||||
self.data = self.data[idx]
|
||||
self.update_index()
|
||||
|
||||
def trim_sphere(self, radius):
|
||||
"""
|
||||
remove atoms outside a given sphere.
|
||||
@ -637,7 +697,7 @@ class Cluster(object):
|
||||
"""
|
||||
idx = self.data['e'] != 0
|
||||
ems = self.data[['x', 'y', 'z', 't']][idx]
|
||||
return map(tuple, ems)
|
||||
return [tuple(em) for em in ems]
|
||||
|
||||
def get_emitter_count(self):
|
||||
"""
|
||||
@ -702,7 +762,7 @@ class Cluster(object):
|
||||
else:
|
||||
self.data['e'] = 0
|
||||
|
||||
pos = self.positions()
|
||||
pos = self.get_positions()
|
||||
# note: np.linalg.norm does not accept axis argument in version 1.7
|
||||
# (check np.version.version)
|
||||
norm = np.sqrt(np.sum(pos**2, axis=1))
|
||||
@ -739,13 +799,14 @@ class Cluster(object):
|
||||
"""
|
||||
self.data['i'] = np.arange(1, self.data.shape[0] + 1)
|
||||
|
||||
def save_to_file(self, f, fmt=FMT_DEFAULT, comment=""):
|
||||
def save_to_file(self, f, fmt=FMT_DEFAULT, comment="", emitters_only=False):
|
||||
"""
|
||||
save the cluster to a file which can be read by the scattering program.
|
||||
|
||||
the method updates the atom index because some file formats require an index column.
|
||||
|
||||
@param f: (string/handle) path name or open file handle of the cluster file.
|
||||
if the filename ends in .gz, the file is saved in compressed gzip format
|
||||
|
||||
@param fmt: (int) file format.
|
||||
must be one of the FMT_ constants.
|
||||
@ -755,7 +816,10 @@ class Cluster(object):
|
||||
not used in other file formats.
|
||||
by default, self.comment is used.
|
||||
|
||||
@remark if the filename ends in .gz, the file is saved in compressed gzip format
|
||||
@param emitters_only: (bool) if True, only atoms marked as emitters are saved.
|
||||
by default, all atoms are saved.
|
||||
|
||||
@return None
|
||||
"""
|
||||
if fmt == FMT_DEFAULT:
|
||||
fmt = self.file_format
|
||||
@ -763,6 +827,13 @@ class Cluster(object):
|
||||
if not comment:
|
||||
comment = self.comment
|
||||
|
||||
self.update_index()
|
||||
if emitters_only:
|
||||
idx = self.data['e'] != 0
|
||||
data = self.data[idx]
|
||||
else:
|
||||
data = self.data
|
||||
|
||||
if fmt == FMT_MSC:
|
||||
file_format = FMT_CLUSTER_MSC
|
||||
fields = FIELDS_CLUSTER_MSC
|
||||
@ -770,16 +841,158 @@ class Cluster(object):
|
||||
elif fmt == FMT_EDAC:
|
||||
file_format = FMT_CLUSTER_EDAC
|
||||
fields = FIELDS_CLUSTER_EDAC
|
||||
header = "%u l(A)" % (self.data.shape[0])
|
||||
header = "{nat} l(A)".format(nat=data.shape[0])
|
||||
elif fmt == FMT_XYZ:
|
||||
file_format = FMT_CLUSTER_XYZ
|
||||
fields = FIELDS_CLUSTER_XYZ
|
||||
header = "{0}\n{1}".format(self.data.shape[0], comment)
|
||||
header = "{nat}\n{com}".format(nat=data.shape[0], com=comment)
|
||||
else:
|
||||
file_format = FMT_CLUSTER_XYZ
|
||||
fields = FIELDS_CLUSTER_XYZ
|
||||
header = "{0}\n{1}".format(self.data.shape[0], comment)
|
||||
header = "{nat}\n{com}".format(nat=data.shape[0], com=comment)
|
||||
|
||||
self.update_index()
|
||||
data = self.data[fields]
|
||||
data = data[fields]
|
||||
np.savetxt(f, data, fmt=file_format, header=header, comments="")
|
||||
|
||||
|
||||
class ClusterGenerator(object):
|
||||
"""
|
||||
cluster generator class.
|
||||
|
||||
this class bundles the cluster methods in one place
|
||||
so that it's easier to exchange them for different kinds of clusters.
|
||||
|
||||
the project must override at least the create_cluster method.
|
||||
if emitters should be run in parallel tasks, the count_emitters method must be implemented as well.
|
||||
"""
|
||||
|
||||
def __init__(self, project):
|
||||
"""
|
||||
initialize the cluster generator.
|
||||
|
||||
@param project: reference to the project object.
|
||||
cluster generators may need to look up project parameters.
|
||||
"""
|
||||
self.project = project
|
||||
|
||||
def count_emitters(self, model, index):
|
||||
"""
|
||||
return the number of emitter configurations for a particular model, scan and symmetry.
|
||||
|
||||
the number of emitter configurations may depend on the model parameters, scan index and symmetry index.
|
||||
by default, the method returns 1, which means that there is only one emitter configuration.
|
||||
|
||||
emitter configurations are mainly a way to distribute the calculations to multiple processes
|
||||
since the resulting diffraction patterns add up incoherently.
|
||||
for this to work, the create_cluster() method must pay attention to the emitter index
|
||||
and generate either a full cluster with all emitters (single process)
|
||||
or a cluster with only a subset of the emitters according to the emitter index (multiple processes).
|
||||
whether all emitters are calculated in one or multiple processes is decided at run-time
|
||||
based on the available resources.
|
||||
|
||||
note that this function returns the number of _configurations_ not _atoms_.
|
||||
an emitter configuration (declared in a Cluster) may include more than one atom.
|
||||
it is up to the project, what is included in a particular configuration.
|
||||
|
||||
to enable multiple emitter configurations, the derived project class must override this method
|
||||
and return a number greater than 1.
|
||||
|
||||
@note in some cases it may be most efficient to call create_cluster and
|
||||
return Cluster.get_emitter_count() of the generated cluster.
|
||||
this is possible because the method is called with emitter index -1.
|
||||
model and index can be passed unchanged to create_cluster.
|
||||
|
||||
@param model (dictionary) model parameters to be used in the calculation.
|
||||
|
||||
@param index (named tuple CalcID) calculation index.
|
||||
the method should consider only the following attributes:
|
||||
@arg @c scan scan index (index into Project.scans)
|
||||
@arg @c sym symmetry index (index into Project.symmetries)
|
||||
@arg @c emit emitter index must be -1.
|
||||
|
||||
@return number of emitter configurations.
|
||||
this implementation returns the default value of 1.
|
||||
"""
|
||||
return 1
|
||||
|
||||
def create_cluster(self, model, index):
|
||||
"""
|
||||
create a Cluster object given the model parameters and calculation index.
|
||||
|
||||
the generated cluster will typically depend on the model parameters.
|
||||
depending on the project, it may also depend on the scan index, symmetry index and emitter index.
|
||||
|
||||
the scan index can be used to generate a different cluster for different scan geometry,
|
||||
e.g., if some atoms can be excluded due to a longer mean free path.
|
||||
if this is not the case for the specific project, the scan index can be ignored.
|
||||
|
||||
the symmetry index may select a particular domain that has a different atomic arrangement.
|
||||
in this case, depending on the value of index.sym, the function must generate a cluster corresponding
|
||||
to the particular domain/symmetry.
|
||||
the method can ignore the symmetry index if the project defines only one symmetry,
|
||||
or if the symmetry does not correspond to a different atomic structure.
|
||||
|
||||
the emitter index selects a particular emitter configuration.
|
||||
depending on the value of the emitter index, the method must react differently:
|
||||
|
||||
1. if the value is -1, return the full cluster and mark all inequivalent emitter atoms.
|
||||
emitters which are reproduced by a symmetry expansion in combine_emitters() should not be marked.
|
||||
the full diffraction scan will be calculated in one calculation.
|
||||
|
||||
2. if the value is greater or equal to zero, generate the cluster with the emitter configuration
|
||||
selected by the emitter index.
|
||||
the index is in the range between 0 and the return value of count_emitters() minus 1.
|
||||
the results of the individual emitter calculations are summed up in combine_emitters().
|
||||
|
||||
the code should ideally be written such that either case yields the same diffraction result.
|
||||
if count_emitters() always returns 1 (default), the second case does not have to be implemented,
|
||||
and the method can ignore the emitter index.
|
||||
|
||||
the method must ignore the region index.
|
||||
|
||||
if the creation of a cluster fails due to invalid model parameters,
|
||||
rather than raising an exception or constraining values,
|
||||
the method should send an error message to the logger and return an empty cluster or None.
|
||||
|
||||
@param model (dictionary) model parameters to be used in the calculation.
|
||||
|
||||
@param index (named tuple CalcID) calculation index.
|
||||
the method should consider only the following attributes:
|
||||
@arg @c scan scan index (index into Project.scans)
|
||||
@arg @c sym symmetry index (index into Project.symmetries)
|
||||
@arg @c emit emitter index.
|
||||
if -1, generate the full cluster and mark all emitters.
|
||||
if greater or equal to zero, the value is a zero-based index of the emitter configuration.
|
||||
|
||||
@return None.
|
||||
sub-classes must return a valid Cluster object or None in case of failure.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class LegacyClusterGenerator(ClusterGenerator):
|
||||
"""
|
||||
cluster generator class for projects that don't declare a generator.
|
||||
|
||||
in previous versions, the create_cluster and count_emitters methods were implemented by the project class.
|
||||
this class redirects generator calls to the project methods
|
||||
providing compatibility to older project code.
|
||||
"""
|
||||
|
||||
def __init__(self, project):
|
||||
super(LegacyClusterGenerator, self).__init__(project)
|
||||
|
||||
def count_emitters(self, model, index):
|
||||
"""
|
||||
redirect the call to the corresponding project method if implemented.
|
||||
"""
|
||||
try:
|
||||
return self.project.count_emitters(model, index)
|
||||
except AttributeError:
|
||||
return 1
|
||||
|
||||
def create_cluster(self, model, index):
|
||||
"""
|
||||
redirect the call to the corresponding project method.
|
||||
"""
|
||||
return self.project.create_cluster(model, index)
|
||||
|
Reference in New Issue
Block a user