コード例 #1
0
    def __init__(self,
                 crystal: Crystal,
                 qpts: np.ndarray,
                 frequencies: Quantity,
                 structure_factors: Quantity,
                 weights: Optional[np.ndarray] = None,
                 temperature: Optional[Quantity] = None) -> None:
        """
        Parameters
        ----------
        crystal
            Lattice and atom information
        qpts
            Shape (n_qpts, 3) float ndarray. Q-point coordinates, in
            fractional coordinates of the reciprocal lattice
        frequencies
            Shape (n_qpts, 3*crystal.n_atoms) float Quantity. Phonon
            frequencies per q-point and mode
        structure_factors
            Shape (n_qpts, 3*crystal.n_atoms) float Quantity. Structure
            factor per q-point and mode
        weights
            Shape (n_qpts,) float ndarray. The weight for each q-point.
            If None, equal weights are assumed
        temperature
            Scalar float Quantity. The temperature used to calculate any
            temperature-dependent parts of the structure factor (e.g.
            Debye-Waller, Bose population factor). None if no
            temperature-dependent effects have been applied
        """
        super().__init__(crystal, qpts, frequencies, weights)
        n_at = crystal.n_atoms
        n_qpts = len(qpts)
        # Check freqs axis 1 shape here - QpointFrequencies doesn't
        # enforce that the number of modes = 3*(number of atoms)
        _check_constructor_inputs(
            [frequencies, structure_factors, temperature],
            [Quantity, Quantity, [Quantity, type(None)]], [(n_qpts, 3 * n_at),
                                                           (n_qpts, 3 * n_at),
                                                           ()],
            ['frequencies', 'structure_factors', 'temperature'])
        self._structure_factors = structure_factors.to(ureg.bohr**2).magnitude
        self.structure_factors_unit = str(structure_factors.units)

        if temperature is not None:
            self._temperature = temperature.to(ureg.K).magnitude
            self.temperature_unit = str(temperature.units)
        else:
            self._temperature = None
            self.temperature_unit = str(ureg.K)
コード例 #2
0
    def from_cell_vectors(cls: CR, cell_vectors: Quantity) -> CR:
        """
        Create a Crystal object from just cell vectors, containing no
        detailed structure information (atomic positions, species,
        masses)

        Parameters
        ----------
        cell_vectors
            Shape (3, 3) float Quantity in length units. Cartesian
            unit cell vectors. cell_vectors[0] = a,
            cell_vectors[:, 0] = x etc.

        Returns
        -------
        crystal
        """
        return cls(cell_vectors, np.array([]), np.array([]),
                   Quantity(np.array([], dtype=np.float64), 'amu'))
コード例 #3
0
    def broaden(self: S1D, x_width: Quantity, shape: str = 'gauss') -> S1D:
        """
        Broaden y_data and return a new broadened spectrum object

        Parameters
        ----------
        x_width
            Scalar float Quantity. The broadening FWHM
        shape
            One of {'gauss', 'lorentz'}. The broadening shape

        Returns
        -------
        broadened_spectrum
            A new Spectrum1D object with broadened y_data

        Raises
        ------
        ValueError
            If shape is not one of the allowed strings
        """
        if shape == 'gauss':
            xsigma = self._gfwhm_to_sigma(x_width, self.get_bin_centres())
            y_broadened = gaussian_filter1d(
                self.y_data.magnitude, xsigma, mode='constant') * ureg(
                    self.y_data_unit)
        elif shape == 'lorentz':
            broadening = _distribution_1d(self.get_bin_centres().magnitude,
                                          x_width.to(
                                              self.x_data_unit).magnitude,
                                          shape=shape)
            y_broadened = correlate1d(self.y_data.magnitude,
                                      broadening,
                                      mode='constant') * ureg(self.y_data_unit)
        else:
            raise ValueError(f"Distribution shape '{shape}' not recognised")

        return Spectrum1D(
            np.copy(self.x_data.magnitude) * ureg(self.x_data_unit),
            y_broadened, deepcopy(
                (self.x_tick_labels)), deepcopy(self.metadata))
コード例 #4
0
def _qpts_cart_to_frac(qpts: Quantity,
                       crystal: Crystal) -> np.ndarray:
    """Convert set of q-points from Cartesian to fractional coordinates

    Parameters
    ----------

    qpts
        Array of q-points in Cartesian coordinates.
    crystal
        Crystal structure determining reciprocal lattice

    Returns
    -------
    np.ndarray
        Dimensionless array of q-points in fractional coordinates
    """
    lattice = crystal.reciprocal_cell()

    return np.linalg.solve(lattice.to(ureg('1/bohr')).magnitude.T,
                           qpts.to(ureg('1/bohr')).magnitude.T
                           ).T
コード例 #5
0
    def __init__(self, crystal: Crystal, qpts: np.ndarray,
                 frequencies: Quantity,
                 weights: Optional[np.ndarray] = None) -> None:
        """
        Parameters
        ----------
        crystal
            Lattice and atom information
        qpts
            Shape (n_qpts, 3) float ndarray. Q-point coordinates
        frequencies
            Shape (n_qpts, n_branches) float Quantity. Frequencies
            per q-point and mode
        weights
            Shape (n_qpts,) float ndarray. The weight for each q-point.
            If None, equal weights are assumed
        """
        _check_constructor_inputs(
            [crystal, qpts], [Crystal, np.ndarray], [(), (-1, 3)],
            ['crystal', 'qpts'])
        n_qpts = len(qpts)
        # Unlike QpointPhononModes and StructureFactor, don't test the
        # frequencies shape against number of atoms in the crystal, as
        # we may only have the cell vectors
        _check_constructor_inputs(
            [frequencies, weights],
            [Quantity, [np.ndarray, type(None)]],
            [(n_qpts, -1), (n_qpts,)],
            ['frequencies', 'weights'])
        self.crystal = crystal
        self.qpts = qpts
        self.n_qpts = n_qpts
        self._frequencies = frequencies.to(ureg.hartree).magnitude
        self.frequencies_unit = str(frequencies.units)

        if weights is not None:
            self.weights = weights
        else:
            self.weights = np.full(self.n_qpts, 1/self.n_qpts)
コード例 #6
0
def mode_gradients_to_widths(mode_gradients: Quantity,
                             cell_vectors: Quantity) -> Quantity:
    """
    Converts mode gradients (units energy/(length^-1)) to an estimate of the
    mode widths (units energy) by using the cell volume and number of q-points
    to estimate the q-spacing. Note that the  number of q-points is determined
    by the size of mode_gradients, so is not likely to give accurate widths if
    the q-points have been symmetry reduced.

    Parameters
    ----------
    mode_gradients
        Shape (n_qpts, n_modes) float Quantity. The mode gradients.
    cell_vectors
        Shape (3, 3) float Quantity. The cell vectors
    """
    cell_volume = Crystal.from_cell_vectors(cell_vectors)._cell_volume()
    modg = mode_gradients.to('hartree*bohr').magnitude
    q_spacing = 2 / (np.cbrt(len(mode_gradients) * cell_volume))
    mode_widths = q_spacing * modg
    return mode_widths * ureg('hartree').to(
        mode_gradients.units / cell_vectors.units)
コード例 #7
0
ファイル: debye_waller.py プロジェクト: mducle/Euphonic
 def temperature(self):
     # See https://pint.readthedocs.io/en/latest/nonmult.html
     return Quantity(self._temperature, ureg('K')).to(self.temperature_unit)
コード例 #8
0
    def calculate_debye_waller(self,
                               temperature: Quantity,
                               frequency_min: Quantity = Quantity(0.01, 'meV'),
                               symmetrise: bool = True) -> DebyeWaller:
        """
        Calculate the 3 x 3 Debye-Waller exponent for each atom over the
        q-points contained in this object

        Parameters
        ----------
        temperature
            Scalar float Quantity. The temperature to use in the
            Debye-Waller calculation
        frequency_min
            Scalar float Quantity in energy units. Excludes frequencies below
            this limit from the calculation, as the calculation contains a
            1/frequency factor which would result in infinite values. This
            also allows negative frequencies to be excluded
        symmetrise
            Whether to symmetrise the Debye-Waller factor based on the
            crystal symmetry operations. Note that if the Debye-Waller
            exponent is not symmetrised, the results may not be the
            same for unfolded and symmetry-reduced q-point grids

        Returns
        -------
        dw
            An object containing the 3x3 Debye-Waller exponent for each
            atom

        Notes
        -----

        As part of the structure factor calculation, the anisotropic
        Debye-Waller factor is defined as:

        .. math::

          e^{-W} = e^{-\\sum_{\\alpha\\beta}{W^{\\kappa}_{\\alpha\\beta}Q_{\\alpha}Q_{\\beta}}}

        The Debye-Waller exponent is defined as
        :math:`W^{\\kappa}_{\\alpha\\beta}` and is independent of Q, so
        for efficiency can be precalculated to be used in the structure
        factor calculation. The Debye-Waller exponent is calculated by [2]_

        .. math::

          W^{\\kappa}_{\\alpha\\beta} =
          \\frac{1}{4M_{\\kappa}\\sum_{q}{weight_q}}
          \\sum_{q\\nu}weight_q\\frac{\\epsilon_{q\\nu\\kappa\\alpha}\\epsilon^{*}_{q\\nu\\kappa\\beta}}
          {\\omega_{q\\nu}}
          coth(\\frac{\\omega_{q\\nu}}{2k_BT})

        Where :math:`\\nu` runs over phonon modes, :math:`\\kappa` runs
        over atoms, :math:`\\alpha,\\beta` run over the Cartesian
        directions, :math:`M_{\\kappa}` is the atom mass,
        :math:`\\epsilon_{q\\nu\\kappa\\alpha}` are the eigenvectors,
        :math:`\\omega_{q\\nu}` are the frequencies, and :math:`weight_q` is
        the per q-point weight. The q-points should be distributed over the
        1st Brillouin Zone.

        .. [2] G.L. Squires, Introduction to the Theory of Thermal Neutron Scattering, Dover Publications, New York, 1996, 34-37
        """

        # Convert units
        kB = (1 * ureg.k).to('hartree/K').magnitude
        n_atoms = self.crystal.n_atoms
        atom_mass = self.crystal._atom_mass
        freqs = self._frequencies
        freq_min = frequency_min.to('hartree').magnitude
        qpts = self.qpts
        evecs = self.eigenvectors
        weights = self.weights
        temp = temperature.to('K').magnitude

        mass_term = 1 / (4 * atom_mass)

        # Mask out frequencies below frequency_min
        freq_mask = np.ones(freqs.shape)
        freq_mask[freqs < freq_min] = 0

        if temp > 0:
            x = freqs / (2 * kB * temp)
            freq_term = 1 / (freqs * np.tanh(x))
        else:
            freq_term = 1 / (freqs)
        dw = np.zeros((n_atoms, 3, 3))
        # Calculating the e.e* term is expensive, do in chunks
        chunk = 1000
        for i in range(int((len(qpts) - 1) / chunk) + 1):
            qi = i * chunk
            qf = min((i + 1) * chunk, len(qpts))

            evec_term = np.real(
                np.einsum('ijkl,ijkm->ijklm', evecs[qi:qf],
                          np.conj(evecs[qi:qf])))

            dw += (np.einsum('i,k,ij,ij,ijklm->klm', weights[qi:qf], mass_term,
                             freq_term[qi:qf], freq_mask[qi:qf], evec_term))

        dw = dw / np.sum(weights)
        if symmetrise:
            dw_tmp = np.zeros(dw.shape)
            (rot, trans,
             eq_atoms) = self.crystal.get_symmetry_equivalent_atoms()
            cell_vec = self.crystal._cell_vectors
            recip_vec = self.crystal.reciprocal_cell().to('1/bohr').magnitude
            rot_cart = np.einsum('ijk,jl,km->ilm', rot, cell_vec,
                                 recip_vec) / (2 * np.pi)
            for s in range(len(rot)):
                dw_tmp[eq_atoms[s]] += np.einsum('ij,kjl,ml->kim', rot_cart[s],
                                                 dw, rot_cart[s])
            dw = dw_tmp / len(rot)

        dw = dw * ureg('bohr**2').to(self.crystal.cell_vectors_unit + '**2')
        return DebyeWaller(self.crystal, dw, temperature)
コード例 #9
0
    def get_symmetry_equivalent_atoms(
        self, tol: Quantity = Quantity(1e-5, 'angstrom')
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Returns the rotational and translational symmetry operations
        as obtained by spglib.get_symmetry, and also the equivalent
        atoms that each atom gets mapped onto for each symmetry
        operation

        Parameters
        ----------
        tol
           Scalar float Quantity in length units. The distance
           tolerance, if the distance between atoms is less than
           this, they are considered to be equivalent. This is
           also passed to spglib.get_symmetry as symprec

        Returns
        -------
        rotations
           Shape (n_symmetry_ops, 3, 3) integer np.ndarray. The
           rotational symmetry matrices as returned by
           spglib.get_symmetry
        translations
           Shape (n_symmetry_ops, 3) float np.ndarray. The
           rotational symmetry matrices as returned by
           spglib.get_symmetry
        equivalent_atoms
           Shape (n_symmetry_ops, n_atoms) integer np.ndarray.
           The equivalent atoms for each symmetry operation. e.g.
           equivalent_atoms[s, i] = j means symmetry operation s
           maps atom i to atom j

        """
        tol_calc = tol.to('bohr').magnitude
        symprec = tol.to(self.cell_vectors_unit).magnitude
        # Sometimes if symprec is very low, even the identity
        # symmetry op won't be found, and None will be returned
        # For some reason this can't always be reproduced
        symm = get_symmetry(self.to_spglib_cell(), symprec=symprec)
        if symm is None:
            raise RuntimeError(f'spglib.get_symmetry returned None with '
                               f'symprec={symprec}. Try increasing tol')
        n_ops = len(symm['rotations'])
        equiv_atoms = np.full((n_ops, self.n_atoms), -1, dtype=np.int32)
        atom_r_symm = (
            np.einsum('ijk,lk->ilj', symm['rotations'], self.atom_r) +
            symm['translations'][:, np.newaxis, :])
        atom_r_symm -= np.floor(atom_r_symm + 0.5)

        species_idx = self.get_species_idx()
        for spec, idx in species_idx.items():
            for i in idx:
                atom_r_symm_i = atom_r_symm[:, i, :]
                # Difference between symmetry-transformed atom i and all
                # other atoms of that species for each symmetry operation
                diff_frac = (atom_r_symm_i[:, np.newaxis, :] -
                             self.atom_r[np.newaxis, idx, :])
                diff_frac -= np.floor(diff_frac + 0.5)
                diff_cart = np.einsum('ijk,kl->ijl', diff_frac,
                                      self._cell_vectors)
                diff_r = np.linalg.norm(diff_cart, axis=2)
                equiv_idx = np.where(diff_r < tol_calc)
                # There should be one matching atom per symm op
                if not np.array_equal(equiv_idx[0], np.arange(n_ops)):
                    for op_idx, diff_r_op in enumerate(diff_r):
                        equiv_idx_op = np.where(diff_r_op < tol_calc)[0]
                        err_info = (f'for {spec} atom at {self.atom_r[i]} for '
                                    f'symmetry op {op_idx}. Rotation '
                                    f'{symm["rotations"][op_idx]} translation '
                                    f'{symm["translations"][op_idx]}')
                        if len(equiv_idx_op) == 0:
                            raise RuntimeError(
                                f'No equivalent atom found {err_info}')
                        elif len(equiv_idx_op) > 1:
                            raise RuntimeError(
                                f'Multiple equivalent atoms found {err_info}')
                equiv_atoms[:, i] = idx[equiv_idx[1]]

        return symm['rotations'], symm['translations'], equiv_atoms
コード例 #10
0
ファイル: test_crystal.py プロジェクト: mducle/Euphonic
class TestCrystalCreation:
    @pytest.fixture(params=[
        get_expected_crystal('quartz'),
        get_expected_crystal('quartz_cv_only'),
        get_expected_crystal('LZO')
    ])
    def create_from_constructor(self, request):
        expected_crystal = request.param
        crystal = Crystal(*expected_crystal.to_constructor_args())
        return crystal, expected_crystal

    @pytest.fixture(params=[
        get_expected_crystal('quartz'),
        get_expected_crystal('quartz_cv_only'),
        get_expected_crystal('LZO')
    ])
    def create_from_dict(self, request):
        expected_crystal = request.param
        d = expected_crystal.to_dict()
        crystal = Crystal.from_dict(d)
        return crystal, expected_crystal

    @pytest.fixture(params=[
        (get_json_file('quartz'), get_expected_crystal('quartz')),
        (get_json_file('quartz_cv_only'),
         get_expected_crystal('quartz_cv_only')),
        (get_json_file('LZO'), get_expected_crystal('LZO'))
    ])
    def create_from_json_file(self, request):
        filename, expected_crystal = request.param
        return get_crystal_from_json_file(filename), expected_crystal

    @pytest.fixture(params=[(Quantity(
        np.array([[2.426176, -4.20226,
                   0.000000], [2.426176, 4.20226, 0.000000],
                  [0.000000, 0.00000, 5.350304]]),
        'angstrom'), get_expected_crystal('quartz_cv_only'))])
    def create_from_cell_vectors(self, request):
        cell_vectors, expected_crystal = request.param
        return Crystal.from_cell_vectors(cell_vectors), expected_crystal

    @pytest.mark.parametrize('crystal_creator', [
        pytest.lazy_fixture('create_from_constructor'),
        pytest.lazy_fixture('create_from_json_file'),
        pytest.lazy_fixture('create_from_dict'),
        pytest.lazy_fixture('create_from_cell_vectors')
    ])
    def test_create(self, crystal_creator):
        crystal, expected_crystal = crystal_creator
        check_crystal(crystal, expected_crystal)

    faulty_elements = [
        ('cell_vectors',
         np.array([[1.23, 2.45, 0.0], [3.45, 5.66, 7.22]]) * ureg('angstrom'),
         ValueError),
        ('cell_vectors',
         get_expected_crystal('quartz').cell_vectors.magnitude * ureg('kg'),
         DimensionalityError),
        ('cell_vectors',
         get_expected_crystal('quartz').cell_vectors.magnitude * ureg(''),
         DimensionalityError),
        ('atom_r', np.array([[0.125, 0.125, 0.125], [0.875, 0.875,
                                                     0.875]]), ValueError),
        ('atom_mass',
         np.array([15.999399987607514, 15.999399987607514, 91.2239999293416]) *
         ureg('amu'), ValueError),
        ('atom_mass',
         get_expected_crystal('quartz').atom_mass.magnitude * ureg('angstrom'),
         DimensionalityError),
        ('atom_mass',
         get_expected_crystal('quartz').atom_mass.magnitude * ureg(''),
         DimensionalityError),
        ('atom_type', np.array(['O', 'Zr', 'La']), ValueError),
    ]

    @pytest.fixture(params=faulty_elements)
    def inject_faulty_elements(self, request):
        faulty_arg, faulty_value, expected_exception = request.param
        crystal = get_expected_crystal('quartz')
        # Inject the faulty value and get a tuple of constructor arguments
        args = crystal.to_constructor_args(**{faulty_arg: faulty_value})
        return args, expected_exception

    def test_faulty_creation(self, inject_faulty_elements):
        faulty_args, expected_exception = inject_faulty_elements
        with pytest.raises(expected_exception):
            Crystal(*faulty_args)
コード例 #11
0
ファイル: plot.py プロジェクト: mducle/Euphonic
 def _get_q_range(data: Quantity) -> float:
     dimensionless_data = data.to(x_unit).magnitude
     return dimensionless_data[-1] - dimensionless_data[0]