phononkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phononkit/__init__.py +169 -0
- phononkit/analysis/__init__.py +37 -0
- phononkit/analysis/irreps.py +399 -0
- phononkit/analysis/structure.py +106 -0
- phononkit/analysis/substitution.py +79 -0
- phononkit/analysis/symmetry.py +137 -0
- phononkit/core/__init__.py +61 -0
- phononkit/core/constants.py +81 -0
- phononkit/core/types.py +93 -0
- phononkit/displacements/__init__.py +26 -0
- phononkit/displacements/generator.py +83 -0
- phononkit/displacements/thermal.py +95 -0
- phononkit/io/__init__.py +23 -0
- phononkit/io/mcif.py +165 -0
- phononkit/io/phonopy_loader.py +157 -0
- phononkit/modes/__init__.py +22 -0
- phononkit/modes/collection.py +151 -0
- phononkit/modes/gauge.py +115 -0
- phononkit/modes/mode.py +99 -0
- phononkit/projections/__init__.py +23 -0
- phononkit/projections/mass_weighted.py +97 -0
- phononkit/projections/orthonormality.py +73 -0
- phononkit/qpoints/__init__.py +55 -0
- phononkit/qpoints/commensurate.py +124 -0
- phononkit/qpoints/grid.py +132 -0
- phononkit/qpoints/path.py +169 -0
- phononkit/qpoints/utils.py +92 -0
- phononkit/supercells/__init__.py +22 -0
- phononkit/supercells/builder.py +77 -0
- phononkit/supercells/phase.py +107 -0
- phononkit-0.1.0.dist-info/METADATA +189 -0
- phononkit-0.1.0.dist-info/RECORD +34 -0
- phononkit-0.1.0.dist-info/WHEEL +4 -0
- phononkit-0.1.0.dist-info/licenses/LICENSE +21 -0
phononkit/__init__.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""phononkit - A clean, modular Python library for phonon analysis.
|
|
2
|
+
|
|
3
|
+
This package provides tools for:
|
|
4
|
+
- Phonon mode representation and analysis
|
|
5
|
+
- Displacement generation and projections
|
|
6
|
+
- Supercell operations with commensurate q-points
|
|
7
|
+
- Structure analysis and symmetry
|
|
8
|
+
- Data I/O (MCIF export, phonopy integration)
|
|
9
|
+
|
|
10
|
+
The package is organized into domain-driven modules:
|
|
11
|
+
- core: Type definitions and constants
|
|
12
|
+
- qpoints: Q-point mathematics and grids
|
|
13
|
+
- modes: Phonon mode representation
|
|
14
|
+
- displacements: Displacement generation
|
|
15
|
+
- projections: Mass-weighted projections
|
|
16
|
+
- supercells: Supercell operations
|
|
17
|
+
- analysis: Structure analysis and symmetry
|
|
18
|
+
- io: Data import/export
|
|
19
|
+
|
|
20
|
+
All modules follow single responsibility principle (< 500 lines per file)
|
|
21
|
+
and strict layering with no circular dependencies.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
from typing import Optional, List
|
|
27
|
+
|
|
28
|
+
from phononkit.modes import PhononMode, PhononModeCollection
|
|
29
|
+
from phononkit.displacements import (
|
|
30
|
+
generate_displacement,
|
|
31
|
+
generate_displaced_structure,
|
|
32
|
+
generate_mode_displacement,
|
|
33
|
+
generate_thermal_displacement,
|
|
34
|
+
generate_thermal_structure,
|
|
35
|
+
)
|
|
36
|
+
from phononkit.io import (
|
|
37
|
+
load_from_phonopy_yaml,
|
|
38
|
+
create_mode_from_phonopy_data,
|
|
39
|
+
export_to_mcif,
|
|
40
|
+
save_mcif_file,
|
|
41
|
+
)
|
|
42
|
+
from phononkit.supercells import (
|
|
43
|
+
build_supercell,
|
|
44
|
+
build_supercell_for_qpoint,
|
|
45
|
+
create_supercell_mode,
|
|
46
|
+
)
|
|
47
|
+
from phononkit.qpoints import (
|
|
48
|
+
is_commensurate_qpoint,
|
|
49
|
+
find_commensurate_qpoints,
|
|
50
|
+
validate_supercell_commensurability,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_structure_for_mode(
|
|
55
|
+
yaml_path: str,
|
|
56
|
+
qpoint: List,
|
|
57
|
+
mode_index: int,
|
|
58
|
+
amplitude: float = 1.0,
|
|
59
|
+
supercell: Optional[List] = None,
|
|
60
|
+
):
|
|
61
|
+
"""Generate displaced structure for a specific phonon mode.
|
|
62
|
+
|
|
63
|
+
Simple one-function API to load phonon data and generate a displaced
|
|
64
|
+
structure for a specific mode at a given q-point, optionally in a supercell.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
yaml_path: Path to phonopy YAML file
|
|
68
|
+
qpoint: Q-point coordinates [qx, qy, qz] (e.g., [0, 0, 0] for Gamma)
|
|
69
|
+
mode_index: Index of the mode at that q-point (0-based)
|
|
70
|
+
amplitude: Displacement amplitude in Angstroms (default: 1.0)
|
|
71
|
+
supercell: Supercell specification (optional):
|
|
72
|
+
- 3-vector [n1, n2, n3] for diagonal matrix (e.g., [2, 2, 2])
|
|
73
|
+
- Full 3x3 matrix [[n1, 0, 0], [0, n2, 0], [0, 0, n3]]
|
|
74
|
+
If None, uses primitive cell
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
ASE Atoms object with displacements applied
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> # Primitive cell - 3rd mode at Gamma with 0.5 Å amplitude
|
|
81
|
+
>>> structure = generate_structure_for_mode(
|
|
82
|
+
... 'phonopy_params.yaml',
|
|
83
|
+
... qpoint=[0, 0, 0],
|
|
84
|
+
... mode_index=2,
|
|
85
|
+
... amplitude=0.5
|
|
86
|
+
... )
|
|
87
|
+
|
|
88
|
+
>>> # 2x2x2 Supercell using diagonal notation
|
|
89
|
+
>>> structure = generate_structure_for_mode(
|
|
90
|
+
... 'phonopy_params.yaml',
|
|
91
|
+
... qpoint=[0, 0, 0],
|
|
92
|
+
... mode_index=2,
|
|
93
|
+
... amplitude=0.5,
|
|
94
|
+
... supercell=[2, 2, 2]
|
|
95
|
+
... )
|
|
96
|
+
|
|
97
|
+
>>> # Save to file
|
|
98
|
+
>>> from ase.io import write
|
|
99
|
+
>>> write('displaced.vasp', structure)
|
|
100
|
+
"""
|
|
101
|
+
import numpy as np
|
|
102
|
+
|
|
103
|
+
# Load phonon data
|
|
104
|
+
primitive_structure, modes = load_from_phonopy_yaml(yaml_path)
|
|
105
|
+
|
|
106
|
+
# Filter modes by q-point
|
|
107
|
+
qpoint_array = np.array(qpoint)
|
|
108
|
+
qpoint_modes = modes.filter_by_qpoint(qpoint_array)
|
|
109
|
+
|
|
110
|
+
# Check if mode_index is valid
|
|
111
|
+
if mode_index >= qpoint_modes.n_modes:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Mode index {mode_index} out of range. "
|
|
114
|
+
f"Only {qpoint_modes.n_modes} modes at q-point {qpoint}."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Select the mode
|
|
118
|
+
selected_mode = qpoint_modes[mode_index]
|
|
119
|
+
|
|
120
|
+
# Handle supercell
|
|
121
|
+
if supercell is not None:
|
|
122
|
+
supercell_array = np.array(supercell)
|
|
123
|
+
|
|
124
|
+
# Convert 3-vector to 3x3 diagonal matrix
|
|
125
|
+
if supercell_array.ndim == 1 and len(supercell_array) == 3:
|
|
126
|
+
supercell_matrix = np.diag(supercell_array)
|
|
127
|
+
elif supercell_array.shape == (3, 3):
|
|
128
|
+
supercell_matrix = supercell_array
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Invalid supercell format. "
|
|
132
|
+
f"Expected 3-vector [n1,n2,n3] or 3x3 matrix, got {supercell}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Create supercell mode (handles tiling and phase factors)
|
|
136
|
+
from phononkit.supercells import create_supercell_mode
|
|
137
|
+
|
|
138
|
+
supercell_mode = create_supercell_mode(selected_mode, supercell_matrix)
|
|
139
|
+
|
|
140
|
+
# Generate displacement for supercell mode
|
|
141
|
+
displaced_structure = generate_mode_displacement(supercell_mode, amplitude)
|
|
142
|
+
else:
|
|
143
|
+
# Use primitive mode directly
|
|
144
|
+
displaced_structure = generate_mode_displacement(selected_mode, amplitude)
|
|
145
|
+
|
|
146
|
+
return displaced_structure
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = [
|
|
150
|
+
"__version__",
|
|
151
|
+
"PhononMode",
|
|
152
|
+
"PhononModeCollection",
|
|
153
|
+
"generate_structure_for_mode", # Simple API
|
|
154
|
+
"generate_displacement",
|
|
155
|
+
"generate_displaced_structure",
|
|
156
|
+
"generate_mode_displacement",
|
|
157
|
+
"generate_thermal_displacement",
|
|
158
|
+
"generate_thermal_structure",
|
|
159
|
+
"load_from_phonopy_yaml",
|
|
160
|
+
"create_mode_from_phonopy_data",
|
|
161
|
+
"export_to_mcif",
|
|
162
|
+
"save_mcif_file",
|
|
163
|
+
"build_supercell",
|
|
164
|
+
"build_supercell_for_qpoint",
|
|
165
|
+
"create_supercell_mode",
|
|
166
|
+
"is_commensurate_qpoint",
|
|
167
|
+
"find_commensurate_qpoints",
|
|
168
|
+
"validate_supercell_commensurability",
|
|
169
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Structure analysis, species substitution, and symmetry.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Atomic correspondence between structures
|
|
5
|
+
- Species substitution handling
|
|
6
|
+
- Symmetry analysis and irreducible representations
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from phononkit.analysis.structure import (
|
|
10
|
+
find_atomic_correspondence,
|
|
11
|
+
apply_atomic_mapping,
|
|
12
|
+
)
|
|
13
|
+
from phononkit.analysis.substitution import (
|
|
14
|
+
create_substitution_map,
|
|
15
|
+
validate_substitution,
|
|
16
|
+
)
|
|
17
|
+
from phononkit.analysis.symmetry import (
|
|
18
|
+
find_degenerate_modes,
|
|
19
|
+
classify_mode_symmetry,
|
|
20
|
+
generate_mode_summary,
|
|
21
|
+
)
|
|
22
|
+
from phononkit.analysis.irreps import (
|
|
23
|
+
analyze_irreps,
|
|
24
|
+
IrRepsEigen,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"find_atomic_correspondence",
|
|
29
|
+
"apply_atomic_mapping",
|
|
30
|
+
"create_substitution_map",
|
|
31
|
+
"validate_substitution",
|
|
32
|
+
"find_degenerate_modes",
|
|
33
|
+
"classify_mode_symmetry",
|
|
34
|
+
"generate_mode_summary",
|
|
35
|
+
"analyze_irreps",
|
|
36
|
+
"IrRepsEigen",
|
|
37
|
+
]
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Irreducible representations (irreps) analysis for phonon modes.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for identifying symmetry labels (irreps)
|
|
4
|
+
for phonon modes, adapted from the better implementation in anaddb_irreps.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import numpy as np
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from typing import List, Optional, Tuple, Dict, Any
|
|
11
|
+
|
|
12
|
+
from ase import Atoms
|
|
13
|
+
from phonopy.phonon.irreps import IrReps, IrRepLabels
|
|
14
|
+
from phonopy.structure.symmetry import Symmetry
|
|
15
|
+
from phonopy.phonon.character_table import character_table
|
|
16
|
+
from phonopy.phonon.degeneracy import degenerate_sets as get_degenerate_sets
|
|
17
|
+
from phonopy.structure.cells import is_primitive_cell
|
|
18
|
+
from phonopy import load as phonopy_load
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IrRepsEigen(IrReps, IrRepLabels):
|
|
22
|
+
"""Irreducible representations analysis from provided eigenvalues/vectors.
|
|
23
|
+
|
|
24
|
+
This class extends phonopy's IrReps and IrRepLabels to work with
|
|
25
|
+
already calculated frequencies and eigenvectors.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
primitive_atoms,
|
|
31
|
+
qpoint,
|
|
32
|
+
freqs,
|
|
33
|
+
eigvecs,
|
|
34
|
+
is_little_cogroup: bool = False,
|
|
35
|
+
symprec: float = 1e-5,
|
|
36
|
+
degeneracy_tolerance: float = 1e-5,
|
|
37
|
+
log_level: int = 0,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize IrRepsEigen.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
primitive_atoms: Phonopy Atoms object (primitive cell)
|
|
43
|
+
qpoint: Q-point in reduced coordinates
|
|
44
|
+
freqs: Frequencies at this q-point (THz)
|
|
45
|
+
eigvecs: Eigenvectors at this q-point
|
|
46
|
+
is_little_cogroup: Whether to use little co-group
|
|
47
|
+
symprec: Symmetry precision
|
|
48
|
+
degeneracy_tolerance: Tolerance for degeneracy detection
|
|
49
|
+
log_level: Verbosity level
|
|
50
|
+
"""
|
|
51
|
+
self._is_little_cogroup = is_little_cogroup
|
|
52
|
+
self._log_level = log_level
|
|
53
|
+
|
|
54
|
+
self._qpoint = np.array(qpoint)
|
|
55
|
+
self._degeneracy_tolerance = degeneracy_tolerance
|
|
56
|
+
self._symprec = symprec
|
|
57
|
+
self._primitive = primitive_atoms
|
|
58
|
+
self._freqs, self._eig_vecs = freqs, eigvecs
|
|
59
|
+
self._character_table = None
|
|
60
|
+
self._verbose = False
|
|
61
|
+
|
|
62
|
+
def run(self) -> bool:
|
|
63
|
+
"""Run the irreps analysis."""
|
|
64
|
+
self._symmetry_dataset = Symmetry(
|
|
65
|
+
self._primitive, symprec=self._symprec
|
|
66
|
+
).dataset
|
|
67
|
+
|
|
68
|
+
if not is_primitive_cell(self._symmetry_dataset.rotations):
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
"Non-primitive cell is used. Your unit cell may be transformed to "
|
|
71
|
+
"a primitive cell by PRIMITIVE_AXIS tag."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
(self._rotations_at_q, self._translations_at_q) = self._get_rotations_at_q()
|
|
75
|
+
|
|
76
|
+
self._g = len(self._rotations_at_q)
|
|
77
|
+
|
|
78
|
+
self._pointgroup_symbol = self._symmetry_dataset.pointgroup
|
|
79
|
+
|
|
80
|
+
(
|
|
81
|
+
self._transformation_matrix,
|
|
82
|
+
self._conventional_rotations,
|
|
83
|
+
) = self._get_conventional_rotations()
|
|
84
|
+
|
|
85
|
+
self._ground_matrices = self._get_ground_matrix()
|
|
86
|
+
self._degenerate_sets = self._get_degenerate_sets()
|
|
87
|
+
self._irreps = self._get_irreps()
|
|
88
|
+
self._characters, self._irrep_dims = self._get_characters()
|
|
89
|
+
|
|
90
|
+
self._ir_labels = None
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
self._pointgroup_symbol in character_table.keys()
|
|
94
|
+
and character_table[self._pointgroup_symbol] is not None
|
|
95
|
+
):
|
|
96
|
+
(
|
|
97
|
+
self._rotation_symbols,
|
|
98
|
+
character_table_of_ptg,
|
|
99
|
+
) = self._get_rotation_symbols(self._pointgroup_symbol)
|
|
100
|
+
self._character_table = character_table_of_ptg
|
|
101
|
+
|
|
102
|
+
if (abs(self._qpoint) < self._symprec).all() and self._rotation_symbols:
|
|
103
|
+
self._ir_labels = self._get_irrep_labels(character_table_of_ptg)
|
|
104
|
+
self._RamanIR_labels = self._get_infrared_raman()
|
|
105
|
+
else:
|
|
106
|
+
self._rotation_symbols = None
|
|
107
|
+
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def _get_degenerate_sets(self):
|
|
111
|
+
deg_sets = get_degenerate_sets(self._freqs, cutoff=self._degeneracy_tolerance)
|
|
112
|
+
return deg_sets
|
|
113
|
+
|
|
114
|
+
def _get_infrared_raman(self):
|
|
115
|
+
"""Compute IR- and Raman-active irreps using symmetry operations."""
|
|
116
|
+
# make symops in cartesian space
|
|
117
|
+
rprim = self._primitive.cell
|
|
118
|
+
gprim = np.linalg.inv(rprim).T
|
|
119
|
+
|
|
120
|
+
# make cartesian symop matrices for each operation in each class
|
|
121
|
+
nclass = len(self._character_table["rotation_list"])
|
|
122
|
+
self._cartesian_rotations_at_q = np.zeros([nclass, 96, 3, 3])
|
|
123
|
+
degenclass = np.zeros(nclass)
|
|
124
|
+
characters_xyz = np.zeros(nclass)
|
|
125
|
+
chardegen_xyz = np.zeros(nclass)
|
|
126
|
+
characters_x2 = np.zeros(nclass)
|
|
127
|
+
chardegen_x2 = np.zeros(nclass)
|
|
128
|
+
iclass = 0
|
|
129
|
+
for opclass in self._character_table["mapping_table"].keys():
|
|
130
|
+
degenclass[iclass] = len(self._character_table["mapping_table"][opclass])
|
|
131
|
+
iop = 0
|
|
132
|
+
for symop in np.array(self._character_table["mapping_table"][opclass][:]):
|
|
133
|
+
self._cartesian_rotations_at_q[iclass][iop] = np.dot(
|
|
134
|
+
rprim, np.dot(symop, gprim.T)
|
|
135
|
+
)
|
|
136
|
+
iop += 1
|
|
137
|
+
|
|
138
|
+
m = self._cartesian_rotations_at_q[iclass][0]
|
|
139
|
+
# get representation characters for x,y,z functions
|
|
140
|
+
characters_xyz[iclass] = np.matrix.trace(m)
|
|
141
|
+
|
|
142
|
+
# get representation characters for quadratic functions
|
|
143
|
+
bigmat = np.zeros([6, 6])
|
|
144
|
+
ibig = 0
|
|
145
|
+
for ixyz in range(3):
|
|
146
|
+
for ixyz_prime in range(ixyz + 1):
|
|
147
|
+
outprod = np.ndarray.flatten(np.outer(m[:, ixyz], m[:, ixyz_prime]))
|
|
148
|
+
bigmat[ibig, :] = [
|
|
149
|
+
outprod[0],
|
|
150
|
+
outprod[1] + outprod[3],
|
|
151
|
+
outprod[4],
|
|
152
|
+
outprod[2] + outprod[6],
|
|
153
|
+
outprod[5] + outprod[7],
|
|
154
|
+
outprod[8],
|
|
155
|
+
]
|
|
156
|
+
ibig += 1
|
|
157
|
+
|
|
158
|
+
characters_x2[iclass] = np.matrix.trace(bigmat)
|
|
159
|
+
chardegen_xyz[iclass] = characters_xyz[iclass] * degenclass[iclass]
|
|
160
|
+
chardegen_x2[iclass] = characters_x2[iclass] * degenclass[iclass]
|
|
161
|
+
iclass += 1
|
|
162
|
+
|
|
163
|
+
xyzlabels = ["x", "y", "z"]
|
|
164
|
+
x2labels = ["x^2", "xy", "y^2", "xz", "yz", "z^2"]
|
|
165
|
+
IR_dict = {"x": None, "y": None, "z": None}
|
|
166
|
+
Raman_dict = {"x^2": [], "xy": [], "y^2": [], "xz": [], "yz": [], "z^2": []}
|
|
167
|
+
|
|
168
|
+
# loop over irreducible representations
|
|
169
|
+
for irreplabel in self._character_table["character_table"].keys():
|
|
170
|
+
irr_char = self._character_table["character_table"][irreplabel]
|
|
171
|
+
len_irr = irr_char[0]
|
|
172
|
+
n_ir = int(np.round(np.dot(irr_char, chardegen_xyz) / self._g))
|
|
173
|
+
n_ram = int(np.round(np.dot(irr_char, chardegen_x2) / self._g))
|
|
174
|
+
|
|
175
|
+
for ixyz in range(3):
|
|
176
|
+
xyzvec = np.zeros(3)
|
|
177
|
+
for iclass in range(len(self._character_table["mapping_table"].keys())):
|
|
178
|
+
opclass = list(self._character_table["mapping_table"].keys())[
|
|
179
|
+
iclass
|
|
180
|
+
]
|
|
181
|
+
degenclass_val = len(
|
|
182
|
+
self._character_table["mapping_table"][opclass][:]
|
|
183
|
+
)
|
|
184
|
+
for iop in range(degenclass_val):
|
|
185
|
+
xyzvec += (
|
|
186
|
+
irr_char[iclass]
|
|
187
|
+
* self._cartesian_rotations_at_q[iclass][iop][ixyz, :]
|
|
188
|
+
)
|
|
189
|
+
xyzvec *= len_irr / self._g
|
|
190
|
+
if np.linalg.norm(xyzvec) > 1.0e-6:
|
|
191
|
+
IR_dict[xyzlabels[ixyz]] = irreplabel
|
|
192
|
+
|
|
193
|
+
ibig = 0
|
|
194
|
+
for ixyz in range(3):
|
|
195
|
+
for ixyz_prime in range(ixyz + 1):
|
|
196
|
+
x2vec = np.zeros(6)
|
|
197
|
+
for iclass in range(
|
|
198
|
+
len(self._character_table["mapping_table"].keys())
|
|
199
|
+
):
|
|
200
|
+
opclass = list(self._character_table["mapping_table"].keys())[
|
|
201
|
+
iclass
|
|
202
|
+
]
|
|
203
|
+
degenclass_val = len(
|
|
204
|
+
self._character_table["mapping_table"][opclass][:]
|
|
205
|
+
)
|
|
206
|
+
for iop in range(degenclass_val):
|
|
207
|
+
m = self._cartesian_rotations_at_q[iclass][iop]
|
|
208
|
+
outprod = np.ndarray.flatten(
|
|
209
|
+
np.outer(m[:, ixyz], m[:, ixyz_prime])
|
|
210
|
+
)
|
|
211
|
+
bigvec = np.array(
|
|
212
|
+
[
|
|
213
|
+
outprod[0],
|
|
214
|
+
outprod[1] + outprod[3],
|
|
215
|
+
outprod[4],
|
|
216
|
+
outprod[2] + outprod[6],
|
|
217
|
+
outprod[5] + outprod[7],
|
|
218
|
+
outprod[8],
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
x2vec += irr_char[iclass] * bigvec
|
|
222
|
+
|
|
223
|
+
x2vec *= len_irr / self._g
|
|
224
|
+
if np.linalg.norm(x2vec) > 1.0e-6:
|
|
225
|
+
Raman_dict[x2labels[ibig]].append(irreplabel)
|
|
226
|
+
ibig += 1
|
|
227
|
+
|
|
228
|
+
return IR_dict, Raman_dict
|
|
229
|
+
|
|
230
|
+
def get_summary_table(self) -> List[Dict[str, Any]]:
|
|
231
|
+
"""Return core mode information as list of dicts."""
|
|
232
|
+
if not hasattr(self, "_freqs"):
|
|
233
|
+
raise RuntimeError("run() must be called before get_summary_table().")
|
|
234
|
+
|
|
235
|
+
q = tuple(float(x) for x in self._qpoint)
|
|
236
|
+
freqs_thz = self._freqs
|
|
237
|
+
conv = 33.35641 # 1 THz -> cm^-1
|
|
238
|
+
n_modes = len(freqs_thz)
|
|
239
|
+
|
|
240
|
+
irreps = getattr(self, "_irreps", None)
|
|
241
|
+
|
|
242
|
+
ir_active_map: Dict[str, bool] = {}
|
|
243
|
+
raman_active_map: Dict[str, bool] = {}
|
|
244
|
+
|
|
245
|
+
raman_ir = getattr(self, "_RamanIR_labels", None)
|
|
246
|
+
if raman_ir is not None:
|
|
247
|
+
ir_dict, raman_dict = raman_ir
|
|
248
|
+
for lbl in ir_dict.values():
|
|
249
|
+
if lbl:
|
|
250
|
+
ir_active_map[lbl] = True
|
|
251
|
+
for labels in raman_dict.values():
|
|
252
|
+
for lbl in labels:
|
|
253
|
+
if lbl:
|
|
254
|
+
raman_active_map[lbl] = True
|
|
255
|
+
|
|
256
|
+
raw_labels = [None] * n_modes
|
|
257
|
+
ir_labels_seq = getattr(self, "_ir_labels", None)
|
|
258
|
+
deg_sets = getattr(self, "_degenerate_sets", None)
|
|
259
|
+
|
|
260
|
+
mode_to_degset = {}
|
|
261
|
+
if deg_sets is not None:
|
|
262
|
+
for set_idx, deg_set in enumerate(deg_sets):
|
|
263
|
+
for mode_idx in deg_set:
|
|
264
|
+
mode_to_degset[mode_idx] = set_idx
|
|
265
|
+
|
|
266
|
+
for band_index in range(n_modes):
|
|
267
|
+
label = None
|
|
268
|
+
if irreps is not None and band_index < len(irreps):
|
|
269
|
+
ir = irreps[band_index]
|
|
270
|
+
if hasattr(ir, "label"):
|
|
271
|
+
label = ir.label
|
|
272
|
+
elif isinstance(ir, dict) and "label" in ir:
|
|
273
|
+
label = ir["label"]
|
|
274
|
+
|
|
275
|
+
if label is None and ir_labels_seq is not None:
|
|
276
|
+
set_idx = mode_to_degset.get(band_index)
|
|
277
|
+
if set_idx is not None and set_idx < len(ir_labels_seq):
|
|
278
|
+
cand = ir_labels_seq[set_idx]
|
|
279
|
+
if isinstance(cand, (tuple, list)) and cand:
|
|
280
|
+
label = cand[0]
|
|
281
|
+
elif isinstance(cand, str):
|
|
282
|
+
label = cand
|
|
283
|
+
raw_labels[band_index] = label
|
|
284
|
+
|
|
285
|
+
if deg_sets is not None:
|
|
286
|
+
for deg_set in deg_sets:
|
|
287
|
+
labels_in_set = {raw_labels[i] for i in deg_set if raw_labels[i]}
|
|
288
|
+
if len(labels_in_set) == 1:
|
|
289
|
+
lbl = labels_in_set.pop()
|
|
290
|
+
for i in deg_set:
|
|
291
|
+
raw_labels[i] = lbl
|
|
292
|
+
|
|
293
|
+
summary = []
|
|
294
|
+
for band_index, f_thz in enumerate(freqs_thz):
|
|
295
|
+
freq_thz = float(f_thz)
|
|
296
|
+
freq_cm1 = freq_thz * conv
|
|
297
|
+
label = raw_labels[band_index]
|
|
298
|
+
is_ir_active = bool(label and ir_active_map.get(label, False))
|
|
299
|
+
is_raman_active = bool(label and raman_active_map.get(label, False))
|
|
300
|
+
|
|
301
|
+
summary.append(
|
|
302
|
+
{
|
|
303
|
+
"qpoint": q,
|
|
304
|
+
"band_index": band_index,
|
|
305
|
+
"frequency_thz": freq_thz,
|
|
306
|
+
"frequency_cm1": freq_cm1,
|
|
307
|
+
"label": label,
|
|
308
|
+
"is_ir_active": is_ir_active,
|
|
309
|
+
"is_raman_active": is_raman_active,
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
return summary
|
|
313
|
+
|
|
314
|
+
def format_summary_table(self, include_header: bool = True) -> str:
|
|
315
|
+
"""Format the summary table as a human-readable string."""
|
|
316
|
+
summary = self.get_summary_table()
|
|
317
|
+
lines = []
|
|
318
|
+
|
|
319
|
+
if summary:
|
|
320
|
+
qx, qy, qz = summary[0]["qpoint"]
|
|
321
|
+
lines.append(f"q-point: [{qx:.4f}, {qy:.4f}, {qz:.4f}]")
|
|
322
|
+
|
|
323
|
+
point_group = getattr(self, "_pointgroup_symbol", None)
|
|
324
|
+
if point_group:
|
|
325
|
+
lines.append(f"Point group: {point_group}")
|
|
326
|
+
|
|
327
|
+
if lines:
|
|
328
|
+
lines.append("")
|
|
329
|
+
|
|
330
|
+
if include_header:
|
|
331
|
+
header = "# qx qy qz band freq(THz) freq(cm-1) label IR Raman"
|
|
332
|
+
lines.append(header)
|
|
333
|
+
|
|
334
|
+
for row in summary:
|
|
335
|
+
qx, qy, qz = row["qpoint"]
|
|
336
|
+
bi = row["band_index"]
|
|
337
|
+
f_thz = row["frequency_thz"]
|
|
338
|
+
f_cm1 = row["frequency_cm1"]
|
|
339
|
+
label = row["label"] or "-"
|
|
340
|
+
ir_flag = "Y" if row["is_ir_active"] else "."
|
|
341
|
+
raman_flag = "Y" if row["is_raman_active"] else "."
|
|
342
|
+
|
|
343
|
+
line = (
|
|
344
|
+
f"{qx:7.4f} {qy:7.4f} {qz:7.4f} {bi:4d} "
|
|
345
|
+
f"{f_thz:10.4f} {f_cm1:11.2f} {label:10s} {ir_flag:^3s} {raman_flag:^5s}"
|
|
346
|
+
)
|
|
347
|
+
lines.append(line)
|
|
348
|
+
|
|
349
|
+
return "\n".join(lines)
|
|
350
|
+
|
|
351
|
+
def get_verbose_output(self) -> str:
|
|
352
|
+
"""Get verbose phonopy-style irreps output."""
|
|
353
|
+
buf = StringIO()
|
|
354
|
+
with contextlib.redirect_stdout(buf):
|
|
355
|
+
show_method = getattr(self, "_show", None) or getattr(self, "show", None)
|
|
356
|
+
if show_method is None:
|
|
357
|
+
print(repr(self))
|
|
358
|
+
else:
|
|
359
|
+
try:
|
|
360
|
+
show_method(True)
|
|
361
|
+
except TypeError:
|
|
362
|
+
show_method()
|
|
363
|
+
return buf.getvalue()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def analyze_irreps(
|
|
367
|
+
primitive_atoms,
|
|
368
|
+
qpoint,
|
|
369
|
+
freqs,
|
|
370
|
+
eigvecs,
|
|
371
|
+
symprec: float = 1e-5,
|
|
372
|
+
degeneracy_tolerance: float = 1e-5,
|
|
373
|
+
log_level: int = 0,
|
|
374
|
+
) -> IrRepsEigen:
|
|
375
|
+
"""Analyze irreps for a single q-point.
|
|
376
|
+
|
|
377
|
+
Parameters:
|
|
378
|
+
primitive_atoms: Phonopy Atoms object
|
|
379
|
+
qpoint: Q-point
|
|
380
|
+
freqs: Frequencies
|
|
381
|
+
eigvecs: Eigenvectors
|
|
382
|
+
symprec: Symmetry precision
|
|
383
|
+
degeneracy_tolerance: Degeneracy tolerance
|
|
384
|
+
log_level: Log level
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
IrRepsEigen object
|
|
388
|
+
"""
|
|
389
|
+
irr = IrRepsEigen(
|
|
390
|
+
primitive_atoms,
|
|
391
|
+
qpoint,
|
|
392
|
+
freqs,
|
|
393
|
+
eigvecs,
|
|
394
|
+
symprec=symprec,
|
|
395
|
+
degeneracy_tolerance=degeneracy_tolerance,
|
|
396
|
+
log_level=log_level,
|
|
397
|
+
)
|
|
398
|
+
irr.run()
|
|
399
|
+
return irr
|