Passed
Push — master ( 8c12c2...4daa36 )
by Daniel
07:46
created

amd.periodicset.PeriodicSet.__str__()   A

Complexity

Conditions 4

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 4
nop 1
1
"""Implements the :class:`PeriodicSet` class representing a periodic
2
set, defined by a motif and unit cell. This models a crystal with a
3
point at the center of each atom.
4
5
This is the type yielded by :class:`amd.CifReader <.io.CifReader>` and
6
:class:`amd.CSDReader <.io.CSDReader>`. A :class:`PeriodicSet` can be
7
passed as the first argument to :func:`amd.AMD() <.calculate.AMD>` or
8
:func:`amd.PDD() <.calculate.PDD>` to calculate its invariants.
9
"""
10
11
from __future__ import annotations
12
from typing import Optional
13
14
import numpy as np
15
16
from .utils import (
17
    cellpar_to_cell,
18
    cellpar_to_cell_2D,
19
    random_cell,
20
)
21
22
__all__ = ['PeriodicSet']
23
24
25
class PeriodicSet:
26
    """A periodic set is a collection of points (motif) which
27
    periodically repeats according to a lattice (unit cell), often
28
    representing a crystal.
29
30
    :class:`PeriodicSet` s are returned by the readers in the
31
    :mod:`.io` module. They can be passed to
32
    :func:`amd.AMD() <.calculate.AMD>` or
33
    :func:`amd.PDD() <.calculate.PDD>` to calculate their invariants.
34
35
    Parameters
36
    ----------
37
    motif : :class:`numpy.ndarray`
38
        Cartesian (orthogonal) coordinates of the motif, shape (no
39
        points, dims).
40
    cell : :class:`numpy.ndarray`
41
        Cartesian (orthogonal) square array representing the unit cell,
42
        shape (dims, dims). Use
43
        :func:`amd.cellpar_to_cell <.utils.cellpar_to_cell>` to convert
44
        6 cell parameters to an orthogonal square matrix.
45
    name : str, optional
46
        Name of the periodic set.
47
    asym_unit : :class:`numpy.ndarray`, optional
48
        Indices for the asymmetric unit, pointing to the motif. Used in
49
        calculating AMD and PDD.
50
    multiplicities : :class:`numpy.ndarray`, optional
51
        Wyckoff multiplicities of points in the asymmetric unit, number
52
        of unique sites generated under symmetries.
53
    types : :class:`numpy.ndarray`, optional
54
        Array of atomic numbers of motif points.
55
    """
56
57
    def __init__(
58
            self,
59
            motif: np.ndarray,
60
            cell: np.ndarray,
61
            name: Optional[str] = None,
62
            asym_unit: Optional[np.ndarray] = None,
63
            multiplicities: Optional[np.ndarray] = None,
64
            types: Optional[np.ndarray] = None
65
    ):
66
        self.motif = motif
67
        self.cell = cell
68
        self.name = name
69
        self.asym_unit = asym_unit
70
        self.multiplicities = multiplicities
71
        self.types = types
72
73
    @property
74
    def ndim(self) -> int:
75
        return self.cell.shape[0]
76
77
    def __str__(self):
78
        m, n = self.motif.shape
79
        m_pl = '' if m == 1 else 's'
80
        n_pl = '' if n == 1 else 's'
81
        name_str = f'{self.name}: ' if self.name is not None else ''
82
        return f'PeriodicSet({name_str}{m} point{m_pl} in {n} dimension{n_pl})'
83
84
    def __repr__(self):
85
86
        motif_str = str(self.motif).replace('\n ', '\n' + ' ' * 11)
87
        cell_str = str(self.cell).replace('\n ', '\n' + ' ' * 10)
88
        optional_attrs = []
89
        for attr in ('asym_unit', 'multiplicities', 'types'):
90
            val = getattr(self, attr)
91
            if val is not None:
92
                st = str(val).replace('\n ', '\n' + ' ' * (len(attr) + 6))
93
                optional_attrs.append(f'{attr}={st}')
94
        optional_attrs_str = ',\n    ' if optional_attrs else ''
95
        optional_attrs_str += ',\n    '.join(optional_attrs)
96
97
        return (
98
            f'PeriodicSet(name={self.name},\n'
99
            f'    motif={motif_str},\n'
100
            f'    cell={cell_str}{optional_attrs_str})'
101
        )
102
103
    def _equal_cell_and_motif(self, other):
104
        """Used for debugging/tests. True if both 1. the unit cells are
105
        (close to) identical, and 2. the motifs are the same shape, and
106
        every point in one motif has a (close to) identical point
107
        somewhere in the other, accounting for pbc.
108
        """
109
110
        tol = 1e-8
111
        if self.cell.shape != other.cell.shape or \
112
           self.motif.shape != other.motif.shape or \
113
           not np.allclose(self.cell, other.cell):
114
            return False
115
116
        cell_inv = np.linalg.inv(self.cell)
117
        fm1 = np.mod(self.motif @ cell_inv, 1)
118
        fm2 = np.mod(other.motif @ cell_inv, 1)
119
        d1 = np.abs(fm2[:, None] - fm1)
120
        d2 = np.abs(d1 - 1)
121
        diffs = np.amax(np.minimum(d1, d2), axis=-1)
122
123
        if not np.all(
124
            (np.amin(diffs, axis=0) <= tol) | (np.amin(diffs, axis=-1) <= tol)
125
        ):
126
            return False
127
128
        return True
129
130
    @staticmethod
131
    def cubic(scale: float = 1.0, dims: int = 3) -> PeriodicSet:
132
        """Returns a :class:`PeriodicSet` representing a cubic lattice.
133
        """
134
        return PeriodicSet(np.zeros((1, dims)), np.identity(dims) * scale)
135
136
    @staticmethod
137
    def hexagonal(scale: float = 1.0, dims: int = 3) -> PeriodicSet:
138
        """ Return a :class:`PeriodicSet` representing a hexagonal
139
        lattice. Dimensions 2 and 3 only.
140
        """
141
142
        if dims == 3:
143
            cellpar = np.array([scale, scale, scale, 90.0, 90.0, 120.0])
144
            cell = cellpar_to_cell(cellpar)
145
        elif dims == 2:
146
            cell = cellpar_to_cell_2D(np.array([scale, scale, 60.0]))
147
        else:
148
            raise NotImplementedError(
149
                'amd.PeriodicSet.hexagonal() only implemented for dimensions '
150
                f'2 and 3, passed {dims}'
151
            )
152
        return PeriodicSet(np.zeros((1, dims)), cell)
153
154
    @staticmethod
155
    def _random(
156
        n_points: int,
157
        length_bounds: tuple = (1.0, 2.0),
158
        angle_bounds: tuple = (60.0, 120.0),
159
        dims: int = 3
160
    ) -> PeriodicSet:
161
        """Return a :class:`PeriodicSet` with a chosen number of
162
        randomly placed points, in a random cell with edges between
163
        ``length_bounds`` and angles between ``angle_bounds``.
164
        Dimensions 2 and 3 only.
165
        """
166
167
        cell = random_cell(
168
            length_bounds=length_bounds, angle_bounds=angle_bounds, dims=dims
169
        )
170
        frac_motif = np.random.uniform(size=(n_points, dims))
171
        return PeriodicSet(frac_motif @ cell, cell)
172
173
174