public release 2.2.0 - see README.md and CHANGES.md for details

This commit is contained in:
2020-09-04 16:22:42 +02:00
parent fbd2d4fa8c
commit 7c61eb1b41
67 changed files with 2934 additions and 682 deletions

View File

@ -0,0 +1,138 @@
"""
@package projects.common.clusters.crystals
cluster generators for some common bulk crystals
@author Matthias Muntwiler, matthias.muntwiler@psi.ch
@copyright (c) 2015-19 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import math
import numpy as np
import os.path
import periodictable as pt
import logging
import pmsco.cluster as cluster
import pmsco.dispatch as dispatch
import pmsco.project as project
from pmsco.helpers import BraceMessage as BMsg
logger = logging.getLogger(__name__)
class ZincblendeCluster(cluster.ClusterGenerator):
def __init__(self, proj):
super(ZincblendeCluster, self).__init__(proj)
self.atomtype1 = 30
self.atomtype2 = 16
self.bulk_lattice = 1.0
self.surface = (1, 1, 1)
@classmethod
def check(cls, outfilename=None, model_dict=None, domain_dict=None):
"""
function to test and debug the cluster generator.
to use this function, you don't need to import or initialize anything but the class.
though the project class is used internally, the result does not depend on any project settings.
@param outfilename: name of output file for the cluster (XYZ format).
the file is written to the same directory where this module is located.
if empty or None, no file is written.
@param model_dict: dictionary of model parameters to override the default values.
@param domain_dict: dictionary of domain parameters to override the default values.
@return: @ref pmsco.cluster.Cluster object
"""
proj = project.Project()
dom = project.ModelSpace()
dom.add_param('dlat', 10.)
dom.add_param('rmax', 5.0)
if model_dict:
dom.start.update(model_dict)
try:
proj.domains[0].update({'zrot': 0.})
except IndexError:
proj.add_domain({'zrot': 0.})
if domain_dict:
proj.domains[0].update(domain_dict)
proj.add_scan("", 'C', '1s')
clu_gen = cls(proj)
index = dispatch.CalcID(0, 0, 0, -1, -1)
clu = clu_gen.create_cluster(dom.start, index)
if outfilename:
project_dir = os.path.dirname(os.path.abspath(__file__))
outfilepath = os.path.join(project_dir, outfilename)
clu.save_to_file(outfilepath, fmt=cluster.FMT_XYZ, comment="{0} {1} {2}".format(cls, index, str(dom.start)))
return clu
def count_emitters(self, model, index):
return 1
def create_cluster(self, model, index):
"""
calculate a specific set of atom positions given the optimizable parameters.
@param model (dict) optimizable parameters
@arg model['dlat'] bulk lattice constant in Angstrom
@arg model['rmax'] cluster radius
@arg model['phi'] azimuthal rotation angle in degrees
@param dom (dict) domain
@arg dom['term'] surface termination
"""
clu = cluster.Cluster()
clu.comment = "{0} {1}".format(self.__class__, index)
clu.set_rmax(model['rmax'])
a_lat = model['dlat']
dom = self.project.domains[index]
try:
term = int(dom['term'])
except ValueError:
term = pt.elements.symbol(dom['term'].strip().number)
if self.surface == (0, 0, 1):
# identity matrix
m = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
elif self.surface == (1, 1, 1):
# this will map the [111] direction onto the z-axis
m1 = np.array([1, -1, 0]) * math.sqrt(1/2)
m2 = np.array([0.5, 0.5, -1]) * math.sqrt(2/3)
m3 = np.array([1, 1, 1]) * math.sqrt(1/3)
m = np.array([m1, m2, m3])
else:
raise ValueError("unsupported surface specification")
# lattice vectors
a1 = np.matmul(m, np.array((1.0, 0.0, 0.0)) * a_lat)
a2 = np.matmul(m, np.array((0.0, 1.0, 0.0)) * a_lat)
a3 = np.matmul(m, np.array((0.0, 0.0, 1.0)) * a_lat)
# basis
b1 = [np.array((0.0, 0.0, 0.0)), (a2 + a3) / 2, (a3 + a1) / 2, (a1 + a2) / 2]
if term == self.atomtype1:
d1 = np.array((0, 0, 0))
d2 = (a1 + a2 + a3) / 4
else:
d1 = -(a1 + a2 + a3) / 4
d2 = np.array((0, 0, 0))
for b in b1:
clu.add_bulk(self.atomtype1, b + d1, a1, a2, a3)
clu.add_bulk(self.atomtype2, b + d2, a1, a2, a3)
return clu

View File

@ -91,7 +91,7 @@ class FCC111Project(mp.Project):
par['V0'] = inner potential
par['Zsurf'] = position of surface
"""
params = mp.Params()
params = mp.CalculatorParams()
params.title = "fcc(111)"
params.comment = "{0} {1}".format(self.__class__, index)
@ -133,11 +133,11 @@ class FCC111Project(mp.Project):
return params
def create_domain(self):
def create_model_space(self):
"""
define the domain of the optimization parameters.
define the model space of the optimization parameters.
"""
dom = mp.Domain()
dom = mp.ModelSpace()
if self.mode == "single":
dom.add_param('rmax', 5.00, 5.00, 15.00, 2.50)
@ -190,7 +190,7 @@ def create_project():
project.scan_dict['alpha'] = {'filename': os.path.join(project_dir, "demo_alpha_scan.etp"),
'emitter': "Ni", 'initial_state': "3s"}
project.add_symmetry({'default': 0.0})
project.add_domain({'default': 0.0})
return project
@ -229,8 +229,9 @@ def set_project_args(project, project_args):
try:
if project_args.initial_state:
project.initial_state = project_args.initial_state
logger.warning(BMsg("override initial states to {0}", project.initial_state))
for scan in project.scans:
scan.initial_state = project_args.initial_state
logger.warning(f"override initial states of all scans to {project_args.initial_state}")
except AttributeError:
pass

384
projects/demo/molecule.py Normal file
View File

@ -0,0 +1,384 @@
"""
@package pmsco.projects.demo.molecule
scattering calculation project for single molecules
the atomic positions are read from a molecule file.
cluster file, emitter (by chemical symbol), initial state and kinetic energy are specified on the command line.
there are no structural parameters.
@author Matthias Muntwiler, matthias.muntwiler@psi.ch
@copyright (c) 2015-20 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
"""
import math
import numpy as np
import os.path
from pathlib import Path
import periodictable as pt
import argparse
import logging
# noinspection PyUnresolvedReferences
from pmsco.calculators.calculator import InternalAtomicCalculator
# noinspection PyUnresolvedReferences
from pmsco.calculators.edac import EdacCalculator
# noinspection PyUnresolvedReferences
from pmsco.calculators.phagen.runner import PhagenCalculator
import pmsco.cluster as cluster
from pmsco.data import calc_modfunc_loess
# noinspection PyUnresolvedReferences
import pmsco.elements.bindingenergy
from pmsco.helpers import BraceMessage as BMsg
import pmsco.project as project
logger = logging.getLogger(__name__)
class MoleculeFileCluster(cluster.ClusterGenerator):
"""
cluster generator based on external file.
work in progress.
"""
def __init__(self, project):
super(MoleculeFileCluster, self).__init__(project)
self.base_cluster = None
def load_base_cluster(self):
"""
load and cache the project-defined coordinate file.
the file path is set in self.project.cluster_file.
the file must be in XYZ (.xyz) or PMSCO cluster (.clu) format (cf. pmsco.cluster module).
@return: Cluster object (also referenced by self.base_cluster)
"""
if self.base_cluster is None:
clu = cluster.Cluster()
clu.set_rmax(120.0)
p = Path(self.project.cluster_file)
ext = p.suffix
if ext == ".xyz":
fmt = cluster.FMT_XYZ
elif ext == ".clu":
fmt = cluster.FMT_PMSCO
else:
raise ValueError(f"unknown cluster file extension {ext}")
clu.load_from_file(self.project.cluster_file, fmt=fmt)
self.base_cluster = clu
return self.base_cluster
def count_emitters(self, model, index):
"""
count the number of emitter configurations.
the method creates the full cluster and counts the emitters.
@param model: model parameters.
@param index: scan and domain are used by the create_cluster() method,
emit decides whether the method returns the number of configurations (-1),
or the number of emitters in the specified configuration (>= 0).
@return: number of emitter configurations.
"""
clu = self.create_cluster(model, index)
return clu.get_emitter_count()
def create_cluster(self, model, index):
"""
import a cluster from a coordinate file (XYZ format).
the method does the following:
- load the cluster file specified by self.cluster_file.
- trim the cluster according to model['rmax'].
- mark the 6 nitrogen atoms at the center of the trimer as emitters.
@param model: rmax is the trim radius of the cluster in units of the surface lattice constant.
@param index (named tuple CalcID) calculation index.
this method uses the domain index to look up domain parameters in
`pmsco.project.Project.domains`.
`index.emit` selects whether a single-emitter (>= 0) or all-emitter cluster (== -1) is returned.
@return pmsco.cluster.Cluster object
"""
self.load_base_cluster()
clu = cluster.Cluster()
clu.copy_from(self.base_cluster)
clu.comment = f"{self.__class__}, {index}"
dom = self.project.domains[index.domain]
# trim
clu.set_rmax(model['rmax'])
clu.trim_sphere(clu.rmax)
# emitter selection
idx_emit = np.where(clu.data['s'] == self.project.scans[index.scan].emitter)
assert isinstance(idx_emit, tuple)
idx_emit = idx_emit[0]
if index.emit >= 0:
idx_emit = idx_emit[index.emit]
clu.data['e'][idx_emit] = 1
# rotation
if 'xrot' in model:
clu.rotate_z(model['xrot'])
elif 'xrot' in dom:
clu.rotate_z(dom['xrot'])
if 'yrot' in model:
clu.rotate_z(model['yrot'])
elif 'yrot' in dom:
clu.rotate_z(dom['yrot'])
if 'zrot' in model:
clu.rotate_z(model['zrot'])
elif 'zrot' in dom:
clu.rotate_z(dom['zrot'])
logger.info(f"cluster for calculation {index}: "
f"{clu.get_atom_count()} atoms, {clu.get_emitter_count()} emitters")
return clu
class MoleculeProject(project.Project):
"""
general molecule project.
the following model parameters are used:
@arg `model['zsurf']` : position of surface above molecule (angstrom)
@arg `model['Texp']` : experimental temperature (K)
@arg `model['Tdeb']` : debye temperature (K)
@arg `model['V0']` : inner potential (eV)
@arg `model['rmax']` : cluster radius (angstrom)
@arg `model['ares']` : angular resolution (degrees, FWHM)
@arg `model['distm']` : dmax for EDAC (angstrom)
the following domain parameters are used.
they can also be specified as model parameters.
@arg `'xrot'` : rotation about x-axis (applied first) (deg)
@arg `'yrot'` : rotation about y-axis (applied after x) (deg)
@arg `'zrot'` : rotation about z-axis (applied after x and y) (deg)
the project parameters are:
@arg `cluster_file` : name of cluster file of template molecule.
default: "dpdi-trimer.xyz"
"""
def __init__(self):
"""
initialize a project instance
"""
super(MoleculeProject, self).__init__()
self.model_space = project.ModelSpace()
self.scan_dict = {}
self.cluster_file = "demo-cluster.xyz"
self.cluster_generator = MoleculeFileCluster(self)
self.atomic_scattering_factory = PhagenCalculator
self.multiple_scattering_factory = EdacCalculator
self.phase_files = {}
self.rme_files = {}
self.modf_smth_ei = 0.5
def create_params(self, model, index):
"""
set a specific set of parameters given the optimizable parameters.
@param model: (dict) optimization parameters
this method requires zsurf, V0, Texp, Tdeb, ares and distm.
@param index (named tuple CalcID) calculation index.
this method formats the index into the comment line.
"""
params = project.CalculatorParams()
params.title = "molecule demo"
params.comment = f"{self.__class__} {index}"
params.cluster_file = ""
params.output_file = ""
initial_state = self.scans[index.scan].initial_state
params.initial_state = initial_state
emitter = self.scans[index.scan].emitter
params.binding_energy = pt.elements.symbol(emitter).binding_energy[initial_state]
params.polarization = "H"
params.z_surface = model['zsurf']
params.inner_potential = model['V0']
params.work_function = 4.5
params.polar_incidence_angle = 60.0
params.azimuthal_incidence_angle = 0.0
params.angular_resolution = model['ares']
params.experiment_temperature = model['Texp']
params.debye_temperature = model['Tdeb']
params.phase_files = self.phase_files
params.rme_files = self.rme_files
# edac_interface only
params.emitters = []
params.lmax = 15
params.dmax = model['distm']
params.orders = [20]
return params
def create_model_space(self):
"""
define the range of model parameters.
see the class description for a list of parameters.
"""
return self.model_space
# noinspection PyUnusedLocal
def calc_modulation(self, data, model):
"""
calculate the modulation function with project-specific smoothing factor
see @ref pmsco.pmsco.project.calc_modulation.
@param data: (numpy.ndarray) experimental data in ETPI, or ETPAI format.
@param model: (dict) model parameters of the calculation task. not used.
@return copy of the data array with the modulation function in the 'i' column.
"""
return calc_modfunc_loess(data, smth=self.modf_smth_ei)
def create_model_space(mode):
"""
define the model space.
"""
dom = project.ModelSpace()
if mode == "single":
dom.add_param('zsurf', 1.20)
dom.add_param('Texp', 300.00)
dom.add_param('Tdeb', 100.00)
dom.add_param('V0', 10.00)
dom.add_param('rmax', 50.00)
dom.add_param('ares', 5.00)
dom.add_param('distm', 5.00)
dom.add_param('wdom1', 1.0)
dom.add_param('wdom2', 1.0)
dom.add_param('wdom3', 1.0)
dom.add_param('wdom4', 1.0)
dom.add_param('wdom5', 1.0)
else:
raise ValueError(f"undefined model space for {mode} optimization")
return dom
def create_project():
"""
create the project instance.
"""
proj = MoleculeProject()
proj_dir = os.path.dirname(os.path.abspath(__file__))
proj.project_dir = proj_dir
# scan dictionary
# to select any number of scans, add their dictionary keys as scans option on the command line
proj.scan_dict['empty'] = {'filename': os.path.join(proj_dir, "../common/empty-hemiscan.etpi"),
'emitter': "N", 'initial_state': "1s"}
proj.mode = 'single'
proj.model_space = create_model_space(proj.mode)
proj.job_name = 'molecule0000'
proj.description = 'molecule demo'
return proj
def set_project_args(project, project_args):
"""
set the project arguments.
@param project: project instance
@param project_args: (Namespace object) project arguments.
"""
scans = []
try:
if project_args.scans:
scans = project_args.scans
else:
logger.error("missing scan argument")
exit(1)
except AttributeError:
logger.error("missing scan argument")
exit(1)
for scan_key in scans:
scan_spec = project.scan_dict[scan_key]
project.add_scan(**scan_spec)
try:
project.cluster_file = os.path.abspath(project_args.cluster_file)
project.cluster_generator = MoleculeFileCluster(project)
except (AttributeError, TypeError):
logger.error("missing cluster-file argument")
exit(1)
try:
if project_args.emitter:
for scan in project.scans:
scan.emitter = project_args.emitter
logger.warning(f"override emitters of all scans to {project_args.emitter}")
except AttributeError:
pass
try:
if project_args.initial_state:
for scan in project.scans:
scan.initial_state = project_args.initial_state
logger.warning(f"override initial states of all scans to {project_args.initial_state}")
except AttributeError:
pass
try:
if project_args.energy:
for scan in project.scans:
scan.energies = np.asarray((project_args.energy, ))
logger.warning(f"override scan energy of all scans to {project_args.energy}")
except AttributeError:
pass
try:
if project_args.symmetry:
for angle in np.linspace(0, 360, num=project_args.symmetry, endpoint=False):
project.add_domain({'xrot': 0., 'yrot': 0., 'zrot': angle})
logger.warning(f"override rotation symmetry to {project_args.symmetry}")
except AttributeError:
pass
def parse_project_args(_args):
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# main arguments
parser.add_argument('--scans', nargs="*",
help="nick names of scans to use in calculation (see create_project function)")
parser.add_argument('--cluster-file',
help="path name of molecule file (xyz format).")
# conditional arguments
parser.add_argument('--emitter',
help="emitter: chemical symbol")
parser.add_argument('--initial-state',
help="initial state term: e.g. 2p1/2")
parser.add_argument('--energy', type=float,
help="kinetic energy (eV)")
parser.add_argument('--symmetry', type=int, default=1,
help="n-fold rotational symmetry")
parsed_args = parser.parse_args(_args)
return parsed_args

View File

@ -17,6 +17,9 @@ import numpy as np
import os.path
import periodictable as pt
from pmsco.calculators.calculator import InternalAtomicCalculator
from pmsco.calculators.edac import EdacCalculator
from pmsco.calculators.phagen.runner import PhagenCalculator
import pmsco.cluster as mc
import pmsco.project as mp
from pmsco.helpers import BraceMessage as BMsg
@ -152,6 +155,17 @@ class TwoatomProject(mp.Project):
self.cluster_generator.model_dict['dAB'] = 'dNNi'
self.cluster_generator.model_dict['th'] = 'pNNi'
self.cluster_generator.model_dict['ph'] = 'aNNi'
self.atomic_scattering_factory = PhagenCalculator
self.multiple_scattering_factory = EdacCalculator
self.phase_files = {}
self.rme_files = {}
self.bindings = {}
self.bindings['N'] = {'1s': 409.9}
self.bindings['B'] = {'1s': 188.0}
self.bindings['Ni'] = {'2s': 1008.6,
'2p': (870.0 + 852.7) / 2, '2p1/2': 870.0, '2p3/2': 852.7,
'3s': 110.8,
'3p': (68.0 + 66.2) / 2, '3p1/2': 68.0, '3p3/2': 66.2}
def create_params(self, model, index):
"""
@ -159,40 +173,40 @@ class TwoatomProject(mp.Project):
@param model: (dict) optimizable parameters
"""
params = mp.Params()
params = mp.CalculatorParams()
params.title = "two-atom demo"
params.comment = "{0} {1}".format(self.__class__, index)
params.cluster_file = ""
params.output_file = ""
params.initial_state = self.scans[index.scan].initial_state
params.spherical_order = 2
initial_state = self.scans[index.scan].initial_state
params.initial_state = initial_state
emitter = self.scans[index.scan].emitter
params.binding_energy = self.bindings[emitter][initial_state]
params.polarization = "H"
params.scattering_level = 5
params.fcut = 15.0
params.cut = 15.0
params.angular_resolution = 0.0
params.lattice_constant = 1.0
params.z_surface = model['Zsurf']
params.phase_files = {self.cluster_generator.atom_types['A']: "",
self.cluster_generator.atom_types['B']: ""}
params.msq_displacement = {self.cluster_generator.atom_types['A']: 0.01,
self.cluster_generator.atom_types['B']: 0.0}
params.planewave_attenuation = 1.0
params.inner_potential = model['V0']
params.work_function = 3.6
params.symmetry_range = 360.0
params.polar_incidence_angle = 60.0
params.azimuthal_incidence_angle = 0.0
params.vibration_model = "P"
params.substrate_atomic_mass = 58.69
params.experiment_temperature = 300.0
params.debye_temperature = 356.0
params.debye_wavevector = 1.7558
params.rme_minus_value = 0.0
if self.phase_files:
state = emitter + initial_state
try:
params.phase_files = self.phase_files[state]
except KeyError:
params.phase_files = {}
logger.warning("no phase files found for {} - using default calculator".format(state))
params.rme_files = {}
params.rme_minus_value = 0.1
params.rme_minus_shift = 0.0
params.rme_plus_value = 1.0
params.rme_plus_shift = 0.0
# used by EDAC only
params.emitters = []
params.lmax = 15
@ -201,11 +215,11 @@ class TwoatomProject(mp.Project):
return params
def create_domain(self):
def create_model_space(self):
"""
define the domain of the optimization parameters.
"""
dom = mp.Domain()
dom = mp.ModelSpace()
if self.mode == "single":
dom.add_param('dNNi', 2.109, 2.000, 2.250, 0.050)
@ -308,7 +322,7 @@ def set_project_args(project, project_args):
project.add_scan(**scan_spec)
logger.info(BMsg("add scan {filename} ({emitter} {initial_state})", **scan_spec))
project.add_symmetry({'default': 0.0})
project.add_domain({'default': 0.0})
def parse_project_args(_args):