Test Failed
Push — master ( baece5...0f0bce )
by Daniel
07:51
created

amd.periodicset.PeriodicSet.__str__()   B

Complexity

Conditions 6

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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