#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from collections import defaultdict, namedtuple
from copy import deepcopy
from typing import Union, Callable
import numpy as np
import pandas as pd
from scipy.interpolate import UnivariateSpline
from scipy.optimize import minimize_scalar
from .normalizer import Normalizer
from .utils import UnitConvert, parse_formula, mixed_atomic_weight, logspace
from .vibration import ClusterVibration
__all__ = ['Sample']
[docs]class Sample(defaultdict):
"""
Sample is a object which store a group of configuration data for calculation
"""
def __init__(self,
label: str,
host: str,
impurity: str,
*,
temperature: list = None,
energies: pd.DataFrame = None,
clusters: dict = None,
mean: str = 'arithmetic',
vibration: bool = True,
skip: bool = False,
x_1: float = 0.001,
condition: float = 1e-07,
r_0: Union[float, dict, str] = None,
normalizer: Union[dict, Normalizer] = None):
"""
Parameters
----------
label : str
The Sample label.
host : str, optional
The column name of host energies in ``energies``.
host : str, optional
The column name of impurity energies in ``energies``.
temperature : list, optional
Temperature steps follow the format [start, stop, # of steps].
By default ``None``.
energies : pd.DataFrame, optional
A :py:class:`pandas.DataFrame` object contain raw energies.
By default ``None``.
clusters : dict, optional
Set how to calculate interaction energy.
By default ``None``.
mean : str, optional
Specific how to mix atom weights.
Can be 'arithmetic', 'harmonic', and 'geometric'.
By default 'arithmetic'.
vibration : bool, optional
Specific whether or not to import the thermal vibration effect.
By default ``True``.
skip : bool, optional
Set to true to skip this series sample.
By default ``False``.
x_1 : float, optional
The initialization concentration of impurity.
By default ``0.001``.
condition : float, optional
Convergence condition.
By default ``1e-07``.
r_0 : Union[float, dict, str], optional
Set how to estimate r_0 from the given T and c.
If ``local``, r_0 will be calculated from each phase respectively.
If constant, will ignore T and c.
If dict, will do a parabolic curve fitting.
By default ``None``.
normalizer : Union[dict, Normalizer], optional
Configuration of a :py:class:`.Normalizer` or an instance.
If given, this will be used to normalize the long-range interaction energy.
By default ``None``.
"""
super().__init__()
self.label = label
self.mean = mean
self.vibration = vibration
self.condition = condition
self.skip = skip
self.host = host
self.impurity = impurity
self.x_1 = x_1
# ##########
# private vars
# ##########
self._en_min = defaultdict(float)
self._r_0 = r_0
self._ens = None
self._lattice_func = None
self._int_func = None
self._clusters = None
self._normalizer = None
self._temp = None
if normalizer is not None:
self.set_normalizer(normalizer)
if temperature is not None:
self._temp = self.set_temperature(temperature)
if clusters is not None:
self.set_clusters(**clusters)
if energies is not None:
self.set_energies(energies)
[docs] def set_energies(self, energies):
if isinstance(energies, pd.DataFrame):
self._ens = energies
# calculate debye function
host = energies[self.host]
impurity = energies[self.impurity]
xs = UnitConvert.lc2ad(energies.index.values)
# get minimum from a polynomial
poly_min = minimize_scalar(
UnivariateSpline(xs, host, k=4), bounds=(xs[0], xs[-1]), method='bounded')
self._en_min[self.host] = poly_min.fun
poly_min = minimize_scalar(
UnivariateSpline(xs, impurity, k=4), bounds=(xs[0], xs[-1]), method='bounded')
self._en_min[self.impurity] = poly_min.fun
for c in energies:
if c in [self.host, self.impurity]:
continue
comp = parse_formula(c)
ys = energies[c]
mass, num = mixed_atomic_weight(c, mean=self.mean)
for k, v in comp.items():
ys -= self._en_min[k] * v
self[c] = ClusterVibration(
label=c,
xs=xs,
ys=host * num + ys,
mass=mass,
num=num,
)
setattr(self, c, self[c])
if self._normalizer is not None and c in self._normalizer:
ys += self._normalizer[c]
c = f'{c}_'
self[c] = ClusterVibration(
label=c,
xs=xs,
ys=host * num + ys,
mass=mass,
num=num,
)
setattr(self, c, self[c])
else:
raise TypeError(
'energies must be <pd.DataFrame> but got %s' % energies.__class__.__name__)
[docs] def set_temperature(self, temp):
if isinstance(temp, dict):
if 'log_scale' not in temp or not temp['log_scale']:
return np.linspace(temp['start'], temp['stop'], temp['steps'])
return logspace(temp['start'], temp['stop'], temp['steps'])
if isinstance(temp, (list, np.ndarray)):
return deepcopy(temp)
if isinstance(temp, (float, int)):
return [temp]
raise NameError('temperature was configured in error format')
@property
def energies(self):
return self._ens
@property
def clusters(self):
return deepcopy(self._clusters)
[docs] def set_clusters(self, **val):
self._clusters = deepcopy(val)
@property
def normalizer(self):
return self._normalizer
@property
def temperatures(self):
return self._temp
[docs] def set_normalizer(self, val):
if isinstance(val, Normalizer):
pass
elif isinstance(val, dict):
val = Normalizer(**val)
else:
raise TypeError('normalizer must be a dict or has type of <Normalizer> but got %s' %
val.__class__.__name__)
self._normalizer = val
if self._ens is not None:
host = self._ens[self.host]
xs = UnitConvert.lc2ad(self._ens.index.values)
for c, v in self._normalizer.items():
if c in self._ens:
ys = self._ens[c] + v
comp = parse_formula(c)
mass, num = mixed_atomic_weight(c, mean=self.mean)
for k_, v_ in comp.items():
ys -= self._en_min[k_] * v_
c = f'{c}_'
self[c] = ClusterVibration(
label=c,
xs=xs,
ys=ys + host * num,
mass=mass,
num=num,
)
setattr(self, c, self[c])
[docs] def __call__(self,
*,
T: float = None,
r: [float, dict, str, None] = None,
vibration: bool = None,
energy_patch: Callable[[float, float], namedtuple] = None,
**kwargs):
"""Get interaction energies at given T, and r.
Parameters
----------
T : float
Temperature.
r : float, dict, str, or None, optional
Atomic distance. By default ``None``.
vibration: bool
Specific whether or not to import the thermal vibration effect.
energy_patch: Callable[[float, float], namedtuple], optional
A patch that will be used to correct the returns of interaction energy.
By default ``None``.
Returns
-------
namedtuple: namedtuple
Named tuple contains calculated interaction energies.
"""
del kwargs
def _int(cluster, r_):
ret_ = 0
for k, v in cluster.items():
if vibration:
ret_ += self[k](T=T, r=r_) * v
else:
ret_ += self[k](r=r_) * v
return ret_
if vibration is None:
vibration = self.vibration
if r is None:
r = self._r_0
if T is None:
vibration = False
ret = {}
for k, v in self._clusters.items():
try:
ret[k] = _int(v, r)
except KeyError as e:
raise KeyError(f'configuration of `{k}` in parameter <series.clusters> '
f'reference an unknown phase {e}')
if energy_patch is not None:
patch = energy_patch(T, r)
for k, v in patch.items():
if k in ret:
ret[k] += v
return namedtuple('interaction_energy', self._clusters.keys())(**ret)
[docs] def ite(self,
*,
temperature: list = None,
k: int = 3,
vibration: bool = None,
r_0: [float, dict, str, None] = None,
**kwargs):
"""Iterate over each temperature
Parameters
----------
temperature : list, optional
Reset temperature steps, by default None
k: int
Degree of the smoothing spline. Must be <= 5. Default is k=3, a cubic spline.
vibration: bool
Specific whether or not to import the thermal vibration effect.
Yields
-------
T: float
Temperature at current step.
r_func: Callable[[float], float]
A function receiving impurity concentration c and
returns corresponding atomic distance r.
"""
del kwargs
def r_0_func(t):
x_mins = []
c_mins = []
for k_, v in r_0.items():
if vibration:
_, x_min = self[k_](T=t, min_x='ws')
else:
_, x_min = self[k_](min_x='ws')
x_mins.append(x_min)
c_mins.append(v)
tmp = np.array([c_mins, x_mins])
index = np.argsort(tmp[0])
return UnivariateSpline(tmp[0, index], tmp[1, index], k=k)
if vibration is None:
vibration = self.vibration
if r_0 is None:
r_0 = self._r_0
if temperature is not None:
temperature = self.set_temperature(temperature)
else:
temperature = self._temp
for t in temperature:
if isinstance(r_0, dict):
yield t, r_0_func(t)
elif isinstance(r_0, str) and r_0 == 'local':
yield t, lambda _: r_0
elif isinstance(r_0, (float, int)):
yield t, lambda _: UnitConvert.lc2ad(r_0)
else:
raise RuntimeError('r_0 must be type of `dict`, `float` or str `local`')
def __repr__(self):
s1 = ' | \n |-'
s2 = ' | '
header = [f'{self.label}--<skip: {self.skip}>:']
return f'\n{s1}'.join(header + [f'\n{s2}'.join(str(self.normalizer).split('\n'))] +
[f'\n{s2}'.join(str(v).split('\n')) for v in self.values()])