Loading...
Searching...
No Matches
symmetry.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Crystal symmetry via spglib.
5 *
6 * Provides spacegroup detection, symmetry operations, and IBZ k-point reduction.
7 *
8 * Space-group detection is delegated to spglib, cited as its documentation
9 * asks: A. Togo, K. Shinohara & I. Tanaka, Sci. Technol. Adv. Mater.: Methods
10 * 4, 2384822 (2024).
11 */
12
13#include <cmath>
14#include <spglib.h>
15#include <stdexcept>
16#include <string>
17#include <vector>
18
25
26namespace q3 {
27
28namespace detail {
29// Convert Crystal to spglib cell format
30inline void crystal_to_spglib(const Crystal& crystal,
31 double lattice[3][3],
32 std::vector<double>& positions,
33 std::vector<int>& types) {
34 Real lattice_parameter = crystal.lattice_parameter();
35 const Matrix3x3& direct_lattice_vectors = crystal.lattice_vectors();
36
37 // spglib uses column-major lattice[cartesian_coordinate][basis_vector_index]
38
39 // Our direct_lattice_vectors are stored as rows:
40
41 // direct_lattice_vectors[basis_vector_index][cartesian_coordinate] Therefore, we transpose the
42
43 // matrix to pass it to spglib.
44 for (int coordinate_index = 0; coordinate_index < 3; coordinate_index++) {
45 for (int basis_index = 0; basis_index < 3; basis_index++) {
46 lattice[coordinate_index][basis_index] =
47 direct_lattice_vectors[basis_index][coordinate_index] *
48 lattice_parameter; // in Bohr
49 }
50 }
51
52 int number_of_atoms = crystal.number_of_atoms();
53 positions.resize(3 * number_of_atoms);
54 types.resize(number_of_atoms);
55 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
56 Vector3D fractional_position = crystal.fractional_position(atom_index);
57 positions[3 * atom_index] = fractional_position.x;
58 positions[3 * atom_index + 1] = fractional_position.y;
59 positions[3 * atom_index + 2] = fractional_position.z;
60 types[atom_index] = crystal.site_symmetry_class(atom_index) + 1; // spglib: 1-based
61 }
62}
63} // namespace detail
64
65/// Symmetry analysis and IBZ k-point reduction using spglib.
67 public:
68 /**
69 * Analyze symmetry of a crystal structure.
70 * Returns the international spacegroup symbol.
71 */
72 static std::string
73 find_spacegroup(const Crystal& crystal,
75 double lattice[3][3];
76 std::vector<double> positions;
77 std::vector<int> types;
78 detail::crystal_to_spglib(crystal, lattice, positions, types);
79
80 SpglibDataset* dataset = spg_get_dataset(lattice,
81 reinterpret_cast<double(*)[3]>(positions.data()),
82 types.data(),
83 crystal.number_of_atoms(),
84 symmetry_precision);
85
86 if (!dataset) {
87 log::warn("spglib: failed to determine spacegroup");
88 return "Unknown";
89 }
90
91 std::string symbol(dataset->international_symbol);
92 int spacegroup_number = dataset->spacegroup_number;
93 int number_of_operations = dataset->n_operations;
94
95 spg_free_dataset(dataset);
96
97 log::info(" Spacegroup: {} (#{}, {} symmetry ops)",
98 symbol,
99 spacegroup_number,
100 number_of_operations);
101 return symbol;
102 }
103
104 /// Get the number of symmetry operations.
106 const Crystal& crystal,
108 double lattice[3][3];
109 std::vector<double> positions;
110 std::vector<int> types;
111 detail::crystal_to_spglib(crystal, lattice, positions, types);
112
113 SpglibDataset* dataset = spg_get_dataset(lattice,
114 reinterpret_cast<double(*)[3]>(positions.data()),
115 types.data(),
116 crystal.number_of_atoms(),
117 symmetry_precision);
118
119 if (!dataset)
120 return 1;
121 int number_of_operations = dataset->n_operations;
122 spg_free_dataset(dataset);
123 return number_of_operations;
124 }
125
126 /**
127 * Reduce a uniform k-grid to the irreducible Brillouin zone.
128 * Returns a KPointsGrid with reduced k-points and correct weights.
129 */
130 static KPointsGrid
131 reduce_kpoints(const Crystal& crystal,
132 const KPointsGrid& input_grid,
134 const auto& dimensions = input_grid.grid_dimensions();
135 const auto& shifts = input_grid.grid_shifts();
136 int grid_dimension_x = dimensions[0];
137 int grid_dimension_y = dimensions[1];
138 int grid_dimension_z = dimensions[2];
139 int shift_x = shifts[0];
140 int shift_y = shifts[1];
141 int shift_z = shifts[2];
142
143 double lattice[3][3];
144 std::vector<double> positions;
145 std::vector<int> types;
146 detail::crystal_to_spglib(crystal, lattice, positions, types);
147
148 int mesh[3] = {grid_dimension_x, grid_dimension_y, grid_dimension_z};
149 int is_shift[3] = {shift_x, shift_y, shift_z};
150 int is_time_reversal = 1;
151
152 int total_grid_points = grid_dimension_x * grid_dimension_y * grid_dimension_z;
153 std::vector<int> mapping(total_grid_points);
154 std::vector<int> grid_address_flat(total_grid_points * 3);
155 auto* grid_address = reinterpret_cast<int(*)[3]>(grid_address_flat.data());
156
157 int number_of_operations = number_of_symmetry_operations(crystal, symmetry_precision);
158
159 int number_of_irreducible_kpoints =
160 spg_get_ir_reciprocal_mesh(grid_address,
161 mapping.data(),
162 mesh,
163 is_shift,
164 is_time_reversal,
165 lattice,
166 reinterpret_cast<double(*)[3]>(positions.data()),
167 types.data(),
168 crystal.number_of_atoms(),
169 symmetry_precision);
170
171 if (number_of_irreducible_kpoints <= 0) {
172 log::warn("spglib: reciprocal mesh generation failed, using full grid");
173 KPointsGrid fallback_grid;
174 fallback_grid.generate(
175 grid_dimension_x, grid_dimension_y, grid_dimension_z, shift_x, shift_y, shift_z);
176 return fallback_grid;
177 }
178
179 // Collect irreducible k-points and their weights
180 std::vector<int> irreducible_indices;
181 std::vector<int> weights;
182 for (int grid_index = 0; grid_index < total_grid_points; grid_index++) {
183 if (mapping[grid_index] == grid_index) {
184 irreducible_indices.push_back(grid_index);
185 weights.push_back(0);
186 }
187 }
188
189 for (int grid_index = 0; grid_index < total_grid_points; grid_index++) {
190 int representative = mapping[grid_index];
191 for (size_t irreducible_index = 0; irreducible_index < irreducible_indices.size();
192 irreducible_index++) {
193 if (irreducible_indices[irreducible_index] == representative) {
194 weights[irreducible_index]++;
195 break;
196 }
197 }
198 }
199
200 // Build reduced KPointsGrid
201 KPointsGrid kpoints;
202 kpoints.set_grid_metadata(dimensions, shifts);
203 for (int irreducible_index = 0; irreducible_index < number_of_irreducible_kpoints;
204 irreducible_index++) {
205 int grid_index = irreducible_indices[irreducible_index];
206 KPoint kpoint_data;
207 kpoint_data.coordinates.x =
208 static_cast<Real>(grid_address[grid_index][0] + is_shift[0] / 2.0) / mesh[0];
209 kpoint_data.coordinates.y =
210 static_cast<Real>(grid_address[grid_index][1] + is_shift[1] / 2.0) / mesh[1];
211 kpoint_data.coordinates.z =
212 static_cast<Real>(grid_address[grid_index][2] + is_shift[2] / 2.0) / mesh[2];
213
214 // Fold back to first BZ
215 for (int coordinate_index = 0; coordinate_index < 3; coordinate_index++) {
216 kpoint_data.coordinates[coordinate_index] -=
217 std::floor(kpoint_data.coordinates[coordinate_index] + 0.5 +
219 }
220
221 kpoint_data.weight = static_cast<Real>(weights[irreducible_index]) / total_grid_points *
222 2.0; // factor 2 for spin
223 kpoints.add_kpoint(kpoint_data);
224 }
225
226 log::info(" IBZ reduction: {} -> {} k-points ({} symmetry ops)",
227 total_grid_points,
228 number_of_irreducible_kpoints,
229 number_of_operations);
230 kpoints.pretty_print();
231
232 return kpoints;
233 }
234
235 /**
236 *
237 * Reduction rule: iterate k-points in order, and for each unvisited k-point, apply
238 * all symmetry operations (+ time reversal) to find and mark equivalent k-points
239 * with higher indices. The representative for each equivalence class is the
240 * lowest-index member. Note this reduces in a single pass with the crystal's own
241 * point group; pw.x instead reduces with the Bravais lattice group and restores the
242 * k-points the crystal group does not relate in a second pass, so the two are not
243 * the same rule even though the inner loop is.
244 *
245 * Unlike reduce_kpoints() which delegates to spg_get_ir_reciprocal_mesh(), this method
246 * uses Q3's own symmetry operations (from spglib's spg_get_dataset, via
247 * get_symmetry_operations()) and performs the equivalence checking internally, ensuring
248 * identical IBZ representatives.
249 *
250 * Uses the actual (atomic-basis-aware) crystal point group, not the Bravais lattice's
251 * holohedral group: for any crystal whose basis reduces the symmetry below the lattice's
252 * (e.g. a finite-difference-displaced structure, or any multi-atom basis at a
253 * less-than-maximal-symmetry arrangement), the lattice's holohedral rotations include
254 * operations that are not actual symmetries of the decorated crystal -- treating k-points
255 * related only by one of those extra operations as equivalent silently corrupts the
256 * k-point sum (see TODOs.md's "IBZ k-point vector reconstruction" writeup). Reuses
257 * reciprocal_rotation (= (R^{-1})^T), the same convention already used for G-vector
258 * transformation in the density symmetrizer, since k-vectors are reciprocal-space
259 * quantities, not real-space positions -- crystallographic rotation matrices in a
260 * (generally oblique) fractional basis are not self-inverse-transpose.
261 */
263 const Crystal& crystal,
264 const KPointsGrid& input_grid,
265 bool use_time_reversal = true,
267 const auto& dimensions = input_grid.grid_dimensions();
268 const auto& shifts = input_grid.grid_shifts();
269 int grid_dimension_1 = dimensions[0];
270 int grid_dimension_2 = dimensions[1];
271 int grid_dimension_3 = dimensions[2];
272 int shift_1 = shifts[0];
273 int shift_2 = shifts[1];
274 int shift_3 = shifts[2];
275
276 int total_grid_points = grid_dimension_1 * grid_dimension_2 * grid_dimension_3;
277
278 /**
279 * The crystal's actual (basis-aware) point group -- NOT
280 * compute_bravais_lattice_rotations(), which only looks at lattice vectors and ignores
281 * whether the atomic decoration actually respects each candidate rotation.
282 */
283 auto symmetry_operations = get_symmetry_operations(crystal, symmetry_precision);
284 int number_of_operations = static_cast<int>(symmetry_operations.size());
285
286 /**
287 * The mesh is the one the caller already generated, read rather than
288 * rebuilt. Reconstructing it here meant a second construction that had
289 * to agree with KPointsGrid::generate() on ordering, on the shift
290 * convention and on zone folding, with nothing checking that it did --
291 * and the two did not fold identically: this one left coordinates in
292 * [0,1) and folded only on the way out, while the grid folds as it
293 * generates. Reading the list removes the question.
294 *
295 * The numbering is internal either way. It decides only which member of
296 * each equivalence class becomes the representative; the spglib path
297 * (reduce_kpoints, selected by ibz_reduction_method) numbers the same
298 * mesh in the opposite order and travels through the same KPointsGrid,
299 * results.json, checkpoint and BerkeleyGW writers.
300 */
301 const std::vector<KPoint>& mesh_kpoints = input_grid.kpoints();
302 if (static_cast<int>(mesh_kpoints.size()) != total_grid_points) {
303 throw std::runtime_error("reduce_kpoints_internal: the input grid holds " +
304 std::to_string(mesh_kpoints.size()) +
305 " k-points but its dimensions imply " +
306 std::to_string(total_grid_points) +
307 ". Call KPointsGrid::generate() before reducing.");
308 }
309
310 const auto crystal_component = [&mesh_kpoints](int flat_index, int dimension) -> double {
311 const Vector3D& coordinates = mesh_kpoints[flat_index].coordinates;
312 return dimension == 0 ? coordinates.x
313 : (dimension == 1 ? coordinates.y : coordinates.z);
314 };
315
316 /**
317 * has_representative[i] records whether k-point i has already been
318 * folded onto an earlier one. Asking that question directly, rather
319 * than encoding it as "the map points at itself", means reading the
320 * bookkeeping never requires knowing the convention first.
321 */
322 std::vector<bool> has_representative(total_grid_points, false);
323 std::vector<double> weight_accumulator(total_grid_points, 0.0);
324
325 constexpr double grid_tolerance = 1.0e-5;
326
327 for (int kpoint_index = 0; kpoint_index < total_grid_points; kpoint_index++) {
328 if (has_representative[kpoint_index]) {
329 continue; // already folded onto a lower-index representative
330 }
331 weight_accumulator[kpoint_index] = 1.0;
332
333 for (int operation_index = 0; operation_index < number_of_operations;
334 operation_index++) {
335 /**
336 * k-vectors are reciprocal-space quantities: use reciprocal_rotation
337 * (= (R^{-1})^T), not rotation (the real-space/position convention) --
338 * crystallographic rotation matrices are not self-inverse-transpose in a
339 * generally oblique fractional basis (e.g. FCC's rhombohedral primitive cell).
340 */
341 const auto& rotation = symmetry_operations[operation_index].reciprocal_rotation;
342
343 // Apply rotation in crystal coordinates: k' = R_reciprocal * k
344 double rotated_crystal[3];
345 for (int dimension = 0; dimension < 3; dimension++) {
346 rotated_crystal[dimension] =
347 rotation[dimension][0] * crystal_component(kpoint_index, 0) +
348 rotation[dimension][1] * crystal_component(kpoint_index, 1) +
349 rotation[dimension][2] * crystal_component(kpoint_index, 2);
350 // Fold back to [0, 1) via nint
351 rotated_crystal[dimension] -= std::round(rotated_crystal[dimension]);
352 }
353
354 // Find which grid point this maps to
355 double fractional_index_1 = rotated_crystal[0] * grid_dimension_1 - 0.5 * shift_1;
356 double fractional_index_2 = rotated_crystal[1] * grid_dimension_2 - 0.5 * shift_2;
357 double fractional_index_3 = rotated_crystal[2] * grid_dimension_3 - 0.5 * shift_3;
358
359 bool is_on_grid =
360 std::abs(fractional_index_1 - std::round(fractional_index_1)) <=
361 grid_tolerance &&
362 std::abs(fractional_index_2 - std::round(fractional_index_2)) <=
363 grid_tolerance &&
364 std::abs(fractional_index_3 - std::round(fractional_index_3)) <= grid_tolerance;
365
366 if (is_on_grid) {
367 int mapped_index_1 =
368 ((static_cast<int>(std::round(fractional_index_1 + 2 * grid_dimension_1))) %
369 grid_dimension_1);
370 int mapped_index_2 =
371 ((static_cast<int>(std::round(fractional_index_2 + 2 * grid_dimension_2))) %
372 grid_dimension_2);
373 int mapped_index_3 =
374 ((static_cast<int>(std::round(fractional_index_3 + 2 * grid_dimension_3))) %
375 grid_dimension_3);
376 int mapped_flat_index = mapped_index_3 + mapped_index_2 * grid_dimension_3 +
377 mapped_index_1 * grid_dimension_2 * grid_dimension_3;
378
379 if (mapped_flat_index > kpoint_index &&
380 !has_representative[mapped_flat_index]) {
381 has_representative[mapped_flat_index] = true;
382 weight_accumulator[kpoint_index] += 1.0;
383 }
384 }
385
386 // Also check -k' (time reversal)
387 if (use_time_reversal) {
388 double negated_fractional_index_1 =
389 -rotated_crystal[0] * grid_dimension_1 - 0.5 * shift_1;
390 double negated_fractional_index_2 =
391 -rotated_crystal[1] * grid_dimension_2 - 0.5 * shift_2;
392 double negated_fractional_index_3 =
393 -rotated_crystal[2] * grid_dimension_3 - 0.5 * shift_3;
394
395 bool is_negated_on_grid =
396 std::abs(negated_fractional_index_1 -
397 std::round(negated_fractional_index_1)) <= grid_tolerance &&
398 std::abs(negated_fractional_index_2 -
399 std::round(negated_fractional_index_2)) <= grid_tolerance &&
400 std::abs(negated_fractional_index_3 -
401 std::round(negated_fractional_index_3)) <= grid_tolerance;
402
403 if (is_negated_on_grid) {
404 int negated_mapped_index_1 =
405 ((static_cast<int>(
406 std::round(negated_fractional_index_1 + 2 * grid_dimension_1))) %
407 grid_dimension_1);
408 int negated_mapped_index_2 =
409 ((static_cast<int>(
410 std::round(negated_fractional_index_2 + 2 * grid_dimension_2))) %
411 grid_dimension_2);
412 int negated_mapped_index_3 =
413 ((static_cast<int>(
414 std::round(negated_fractional_index_3 + 2 * grid_dimension_3))) %
415 grid_dimension_3);
416 int negated_flat_index =
417 negated_mapped_index_3 + negated_mapped_index_2 * grid_dimension_3 +
418 negated_mapped_index_1 * grid_dimension_2 * grid_dimension_3;
419
420 if (negated_flat_index > kpoint_index &&
421 !has_representative[negated_flat_index]) {
422 has_representative[negated_flat_index] = true;
423 weight_accumulator[kpoint_index] += 1.0;
424 }
425 }
426 }
427 }
428 }
429
430 // Collect irreducible k-points and normalize weights
431 double total_weight = 0.0;
432 int number_of_irreducible_kpoints = 0;
433 KPointsGrid kpoints;
434
435 for (int grid_index = 0; grid_index < total_grid_points; grid_index++) {
436 if (!has_representative[grid_index]) {
437 number_of_irreducible_kpoints++;
438 total_weight += weight_accumulator[grid_index];
439
440 KPoint kpoint_data;
441 /**
442 * Already in the first BZ: KPointsGrid::generate() folds to
443 * [-0.5, 0.5) as it builds. Folding again here is not a no-op --
444 * std::round(-0.5) is -1, so a coordinate sitting exactly on the
445 * zone boundary would come back as +0.5 and name the opposite
446 * face. The fold belonged to the mesh this function used to
447 * rebuild for itself, and it left with it.
448 */
449 kpoint_data.coordinates = mesh_kpoints[grid_index].coordinates;
450 kpoint_data.weight = weight_accumulator[grid_index];
451 kpoints.add_kpoint(kpoint_data);
452 }
453 }
454
455 /**
456 * Build final KPointsGrid with normalized weights
457 * w_k = count_k / total * 2 (factor 2 for spin degeneracy)
458 */
459 KPointsGrid normalized_kpoints;
460 normalized_kpoints.set_grid_metadata(dimensions, shifts);
461 for (int kpoint_index = 0; kpoint_index < kpoints.number_of_kpoints(); kpoint_index++) {
462 KPoint kpoint_data;
463 kpoint_data.coordinates = kpoints[kpoint_index].coordinates;
464 kpoint_data.weight = kpoints[kpoint_index].weight / total_weight * 2.0;
465 normalized_kpoints.add_kpoint(kpoint_data);
466 }
467
468 log::info(" IBZ reduction (internal): {} -> {} k-points ({} symmetry ops{})",
469 total_grid_points,
470 number_of_irreducible_kpoints,
471 number_of_operations,
472 use_time_reversal ? " + time reversal" : "");
473 normalized_kpoints.pretty_print();
474
475 return normalized_kpoints;
476 }
477
478 /**
479 * Get all symmetry operations (rotation matrices and fractional translations).
480 * Rotation matrices are in crystal (fractional) coordinates.
481 * Fractional translations are in crystal coordinates.
482 * Includes atom_map for each operation: atom_map[na] = image of atom na.
483 */
484 static std::vector<SymmetryOperation> get_symmetry_operations(
485 const Crystal& crystal,
487 double lattice[3][3];
488 std::vector<double> positions;
489 std::vector<int> types;
490 detail::crystal_to_spglib(crystal, lattice, positions, types);
491
492 SpglibDataset* dataset = spg_get_dataset(lattice,
493 reinterpret_cast<double(*)[3]>(positions.data()),
494 types.data(),
495 crystal.number_of_atoms(),
496 symmetry_precision);
497
498 std::vector<SymmetryOperation> operations;
499 if (!dataset) {
500 log::warn("spglib: failed to get symmetry operations");
501 // Return identity only
502 SymmetryOperation identity;
503 identity.rotation = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
504 identity.reciprocal_rotation = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
505 identity.fractional_translation = {0.0, 0.0, 0.0};
506 operations.push_back(identity);
507 return operations;
508 }
509
510 operations.resize(dataset->n_operations);
511 for (int operation_index = 0; operation_index < dataset->n_operations; operation_index++) {
512 auto& operation = operations[operation_index];
513 // spglib rotation[isym][i][j]: rotated_cryst[i] = sum_j rotation[isym][i][j] * cryst[j]
514 for (int row = 0; row < 3; row++) {
515 for (int column = 0; column < 3; column++) {
516 operation.rotation[row][column] =
517 dataset->rotations[operation_index][row][column];
518 }
519 }
520 operation.fractional_translation.x = dataset->translations[operation_index][0];
521 operation.fractional_translation.y = dataset->translations[operation_index][1];
522 operation.fractional_translation.z = dataset->translations[operation_index][2];
523
524 // Compute reciprocal_rotation = (R^{-1})^T for G-vector transformation.
525
526 // For integer rotation matrices with det(R) = ±1:
527
528 // R^{-1} = adj(R) / det(R)
529
530 // (R^{-1})^T = adj(R)^T / det(R) = cofactor(R) / det(R)
531
532 // But adj(R)^T = cofactor matrix (since adj = cofactor^T).
533
534 // So (R^{-1})^T = cofactor(R) / det(R).
535
536 // Equivalently: (R^{-1})^T_{ij} = adj(R)_{ji} / det(R)
537 const auto& rotation_matrix = operation.rotation;
538
539 // Compute determinant
540 int determinant =
541 rotation_matrix[0][0] * (rotation_matrix[1][1] * rotation_matrix[2][2] -
542 rotation_matrix[1][2] * rotation_matrix[2][1]) -
543 rotation_matrix[0][1] * (rotation_matrix[1][0] * rotation_matrix[2][2] -
544 rotation_matrix[1][2] * rotation_matrix[2][0]) +
545 rotation_matrix[0][2] * (rotation_matrix[1][0] * rotation_matrix[2][1] -
546 rotation_matrix[1][1] * rotation_matrix[2][0]);
547
548 // Compute cofactor matrix elements
549
550 // cofactor[i][j] = (-1)^(i+j) * minor(i,j)
551 int cofactor_matrix[3][3];
552 cofactor_matrix[0][0] = rotation_matrix[1][1] * rotation_matrix[2][2] -
553 rotation_matrix[1][2] * rotation_matrix[2][1];
554 cofactor_matrix[0][1] = -(rotation_matrix[1][0] * rotation_matrix[2][2] -
555 rotation_matrix[1][2] * rotation_matrix[2][0]);
556 cofactor_matrix[0][2] = rotation_matrix[1][0] * rotation_matrix[2][1] -
557 rotation_matrix[1][1] * rotation_matrix[2][0];
558 cofactor_matrix[1][0] = -(rotation_matrix[0][1] * rotation_matrix[2][2] -
559 rotation_matrix[0][2] * rotation_matrix[2][1]);
560 cofactor_matrix[1][1] = rotation_matrix[0][0] * rotation_matrix[2][2] -
561 rotation_matrix[0][2] * rotation_matrix[2][0];
562 cofactor_matrix[1][2] = -(rotation_matrix[0][0] * rotation_matrix[2][1] -
563 rotation_matrix[0][1] * rotation_matrix[2][0]);
564 cofactor_matrix[2][0] = rotation_matrix[0][1] * rotation_matrix[1][2] -
565 rotation_matrix[0][2] * rotation_matrix[1][1];
566 cofactor_matrix[2][1] = -(rotation_matrix[0][0] * rotation_matrix[1][2] -
567 rotation_matrix[0][2] * rotation_matrix[1][0]);
568 cofactor_matrix[2][2] = rotation_matrix[0][0] * rotation_matrix[1][1] -
569 rotation_matrix[0][1] * rotation_matrix[1][0];
570
571 // reciprocal_rotation = (R^{-1})^T = cofactor(R) / det(R)
572
573 // (R^{-1})^T_{ij} = cofactor_{ij} / det
574 for (int row = 0; row < 3; row++) {
575 for (int column = 0; column < 3; column++) {
576 operation.reciprocal_rotation[row][column] =
577 cofactor_matrix[row][column] / determinant;
578 }
579 }
580
581 // Build atom mapping table: atom_map[na] = nb such that
582
583 // R * tau(na) + ft ≡ tau(nb) (mod lattice vector).
584 int number_of_atoms = crystal.number_of_atoms();
585 operation.atom_map.resize(number_of_atoms, -1);
586 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
587 Vector3D tau_cryst = crystal.fractional_position(atom_index);
588 int element_type = crystal.site_symmetry_class(atom_index);
589
590 // Apply rotation + fractional translation in crystal coordinates
591 Vector3D rotated_position;
592 rotated_position.x =
593 rotation_matrix[0][0] * tau_cryst.x + rotation_matrix[0][1] * tau_cryst.y +
594 rotation_matrix[0][2] * tau_cryst.z + operation.fractional_translation.x;
595 rotated_position.y =
596 rotation_matrix[1][0] * tau_cryst.x + rotation_matrix[1][1] * tau_cryst.y +
597 rotation_matrix[1][2] * tau_cryst.z + operation.fractional_translation.y;
598 rotated_position.z =
599 rotation_matrix[2][0] * tau_cryst.x + rotation_matrix[2][1] * tau_cryst.y +
600 rotation_matrix[2][2] * tau_cryst.z + operation.fractional_translation.z;
601
602 // Find which atom matches (modulo lattice vector, same symmetry class)
603 for (int target_atom = 0; target_atom < number_of_atoms; target_atom++) {
604 if (crystal.site_symmetry_class(target_atom) != element_type)
605 continue;
606 Vector3D target_cryst = crystal.fractional_position(target_atom);
607 Vector3D difference = rotated_position - target_cryst;
608 // Reduce to [-0.5, 0.5)
609 difference.x -= std::round(difference.x);
610 difference.y -= std::round(difference.y);
611 difference.z -= std::round(difference.z);
612 if (std::abs(difference.x) < symmetry_precision &&
613 std::abs(difference.y) < symmetry_precision &&
614 std::abs(difference.z) < symmetry_precision) {
615 operation.atom_map[atom_index] = target_atom;
616 break;
617 }
618 }
619 }
620 }
621
622 spg_free_dataset(dataset);
623 return operations;
624 }
625
626 /**
627 * Symmetrize a vector quantity f(3, nat) such as forces.
628 * Convert to crystal coordinates, average over the symmetry operations (using the
629 * rotation matrices and the atom mapping), convert back to Cartesian.
630 * The average is the group-average (Reynolds) projector onto the subspace of fields
631 * invariant under the crystal's point group: an already-symmetric field is an exact
632 * fixed point, and any component the group forbids is projected to zero.
633 */
634 static void symmetrize_vector(const Crystal& crystal,
635 const std::vector<SymmetryOperation>& symmetry_operations,
636 std::vector<Vector3D>& vector_field) {
637 int number_of_symmetry_operations = static_cast<int>(symmetry_operations.size());
639 return;
640
641 int number_of_atoms = crystal.number_of_atoms();
642 const Matrix3x3& direct_lattice_vectors = crystal.lattice_vectors();
643 const Matrix3x3& reciprocal_lattice_vectors = crystal.reciprocal_lattice_vectors();
644
645 // Step 1: convert vector_field from Cartesian to crystal coordinates.
646
647 // A covariant vector is projected onto the direct lattice vectors:
648
649 // work[ipol] = F · a_ipol = Σ_j F[j] * A[ipol][j].
650
651 // In Q3: at[ipol][j] = direct_lattice_vectors[ipol][j]
652 std::vector<Vector3D> work(number_of_atoms);
653 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
654 for (int dimension = 0; dimension < 3; dimension++) {
655 work[atom_index][dimension] =
656 vector_field[atom_index][0] * direct_lattice_vectors[dimension][0] +
657 vector_field[atom_index][1] * direct_lattice_vectors[dimension][1] +
658 vector_field[atom_index][2] * direct_lattice_vectors[dimension][2];
659 }
660 }
661
662 // Step 2: symmetrize in crystal coordinates.
663
664 /**
665 * In crystal coordinates the symmetrized field is
666 * result[atom][i] = Σ_op Σ_j rotation[j][i] * work[atom_map[atom]][j] / n_ops,
667 * i.e. the stored `.rotation` TRANSPOSED, applied to the IMAGE atom's components.
668 *
669 * Why transposed, and why paired with the image. `work` holds the covariant
670 * components w_i = F · a_i. Since an operation maps the lattice onto itself as
671 * M a_i = Σ_j R[j][i] a_j (with R the stored fractional-coordinate rotation for
672 * positions, x' = R x), the covariant components of the ROTATED field are
673 * (R^{-1})^T w. The symmetry-restoring average is the group average
674 * F_sym(a) = (1/n_ops) Σ_op M_op^{-1} F(op(a)),
675 * whose covariant form needs the transform of the INVERSE operation -- which is
676 * R^T -- applied to the components at the image atom op(a) = atom_map[atom].
677 * That combination, and only that combination, leaves an already-symmetric field
678 * unchanged.
679 *
680 * Note R^T is NOT (R^{-1})^T here: an integer crystallographic rotation in a
681 * fractional basis is not orthogonal, so the two differ (the same point this
682 * class makes for k-vectors in reduce_kpoints_internal). Using
683 * `reciprocal_rotation = (rotation^{-1})^T` with the image atom would not be a
684 * fixed point of an already-symmetric field.
685 */
686 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
687 vector_field[atom_index] = {0, 0, 0};
688 }
689
690 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
691 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
692 operation_index++) {
693 const auto& rotation = symmetry_operations[operation_index].rotation;
694 int mapped_atom = symmetry_operations[operation_index].atom_map[atom_index];
695
696 for (int dimension = 0; dimension < 3; dimension++) {
697 vector_field[atom_index][dimension] +=
698 rotation[0][dimension] * work[mapped_atom][0] +
699 rotation[1][dimension] * work[mapped_atom][1] +
700 rotation[2][dimension] * work[mapped_atom][2];
701 }
702 }
703 }
704
705 // Divide by number of symmetry operations
706 Real inverse_number_of_operations = 1.0 / number_of_symmetry_operations;
707 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
708 for (int dimension = 0; dimension < 3; dimension++) {
709 work[atom_index][dimension] =
710 vector_field[atom_index][dimension] * inverse_number_of_operations;
711 }
712 }
713
714 // Step 3: convert back to Cartesian coordinates.
715
716 // Expand the crystal components back on the reciprocal lattice vectors:
717
718 // F'[dim] = Σ_j work[j] * B[j][dim] = (B^T × work)[dim],
719
720 // where B has rows = b_i (reciprocal_lattice_vectors). This is the inverse of Step 1.
721 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
722 for (int dimension = 0; dimension < 3; dimension++) {
723 vector_field[atom_index][dimension] =
724 work[atom_index][0] * reciprocal_lattice_vectors[0][dimension] +
725 work[atom_index][1] * reciprocal_lattice_vectors[1][dimension] +
726 work[atom_index][2] * reciprocal_lattice_vectors[2][dimension];
727 }
728 }
729 }
730
731 /**
732 * Symmetrize a rank-2 tensor (3×3 matrix) such as the stress tensor.
733 * Convert to crystal coordinates, average over the point group, convert back:
734 * T'[i][j] = (1/n_ops) Σ_op Σ_{k,l} rotation[k][i] * rotation[l][j] * T[k][l],
735 * i.e. the same group-average projector symmetrize_vector applies, with both tensor
736 * indices carried instead of one. With no atom index there is no image lookup, and
737 * because the sum runs over the whole group it is unchanged by replacing each
738 * operation with its inverse.
739 */
740 static void symmetrize_matrix(const Crystal& crystal,
741 const std::vector<SymmetryOperation>& symmetry_operations,
742 Matrix3x3& tensor) {
743 int number_of_symmetry_operations = static_cast<int>(symmetry_operations.size());
745 return;
746
747 const Matrix3x3& direct_lattice = crystal.lattice_vectors();
748 const Matrix3x3& reciprocal_lattice = crystal.reciprocal_lattice_vectors();
749
750 // Step 1: Cartesian → crystal coordinates
751
752 // Project both tensor indices onto the direct lattice vectors:
753
754 // work(i,j) = Σ_{k,l} T(k,l) * A[i][k] * A[j][l], where A[i][k] is the k-th
755
756 // Cartesian component of the i-th lattice vector -- i.e. Q3's row-major at[i][k],
757
758 // the transpose of the column-major `at(k,i)` layout Quantum ESPRESSO uses.
759 Matrix3x3 crystal_tensor;
760 for (int row = 0; row < 3; row++) {
761 for (int column = 0; column < 3; column++) {
762 Real sum = 0.0;
763 for (int contracted_row = 0; contracted_row < 3; contracted_row++) {
764 for (int contracted_column = 0; contracted_column < 3; contracted_column++) {
765 sum += tensor[contracted_row][contracted_column] *
766 direct_lattice[row][contracted_row] *
767 direct_lattice[column][contracted_column];
768 }
769 }
770 crystal_tensor[row][column] = sum;
771 }
772 }
773
774 // Step 2: symmetrize in crystal coordinates
775
776 // Both tensor indices are covariant in crystal coordinates, so each operation
777
778 // contributes R^T * T * R, i.e. Σ_{k,l} R[k][i] * R[l][j] * T[k][l] -- R^T being
779
780 // the covariant transform of the inverse operation, as in symmetrize_vector.
781
782 // Summed over the whole group the distinction is immaterial here, since the
783
784 // group contains the inverse of every operation and no atom index is paired in.
785 Matrix3x3 symmetrized;
786 for (int row = 0; row < 3; row++) {
787 for (int column = 0; column < 3; column++) {
788 symmetrized[row][column] = 0.0;
789 }
790 }
791
792 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
793 operation_index++) {
794 const auto& rotation = symmetry_operations[operation_index].rotation;
795 for (int row = 0; row < 3; row++) {
796 for (int column = 0; column < 3; column++) {
797 for (int contracted_row = 0; contracted_row < 3; contracted_row++) {
798 for (int contracted_column = 0; contracted_column < 3;
799 contracted_column++) {
800 symmetrized[row][column] +=
801 rotation[contracted_row][row] *
802 rotation[contracted_column][column] *
803 crystal_tensor[contracted_row][contracted_column];
804 }
805 }
806 }
807 }
808 }
809
810 Real inverse_number_of_operations = 1.0 / number_of_symmetry_operations;
811 for (int row = 0; row < 3; row++) {
812 for (int column = 0; column < 3; column++) {
813 crystal_tensor[row][column] =
814 symmetrized[row][column] * inverse_number_of_operations;
815 }
816 }
817
818 // Step 3: crystal → Cartesian coordinates
819
820 // Expand both tensor indices back on the reciprocal lattice vectors:
821
822 // T(i,j) = Σ_{k,l} work(k,l) * B[k][i] * B[l][j], where B[k][i] is the i-th
823
824 // Cartesian component of the k-th reciprocal vector -- i.e. Q3's row-major
825
826 // bg[k][i], the transpose of the `bg(i,k)` layout Quantum ESPRESSO uses.
827 for (int row = 0; row < 3; row++) {
828 for (int column = 0; column < 3; column++) {
829 Real sum = 0.0;
830 for (int contracted_row = 0; contracted_row < 3; contracted_row++) {
831 for (int contracted_column = 0; contracted_column < 3; contracted_column++) {
832 sum += crystal_tensor[contracted_row][contracted_column] *
833 reciprocal_lattice[contracted_row][row] *
834 reciprocal_lattice[contracted_column][column];
835 }
836 }
837 tensor[row][column] = sum;
838 }
839 }
840 }
841};
842
843} // namespace q3
int site_symmetry_class(int atom_index) const
Definition crystal.hpp:284
const Matrix3x3 & reciprocal_lattice_vectors() const
Definition crystal.hpp:205
Vector3D fractional_position(int atom_index) const
Definition crystal.hpp:269
const Matrix3x3 & lattice_vectors() const
Definition crystal.hpp:202
int number_of_atoms() const
Definition crystal.hpp:209
Real lattice_parameter() const
Definition crystal.hpp:180
void set_grid_metadata(const std::array< int, 3 > &dimensions, const std::array< int, 3 > &shifts)
int number_of_kpoints() const
void add_kpoint(const KPoint &kpoint)
void generate(int grid_dimension_x, int grid_dimension_y, int grid_dimension_z, int shift_x, int shift_y, int shift_z)
const std::vector< KPoint > & kpoints() const
void pretty_print() const
const std::array< int, 3 > & grid_dimensions() const
const std::array< int, 3 > & grid_shifts() const
Symmetry analysis and IBZ k-point reduction using spglib.
Definition symmetry.hpp:66
static std::string find_spacegroup(const Crystal &crystal, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:73
static void symmetrize_vector(const Crystal &crystal, const std::vector< SymmetryOperation > &symmetry_operations, std::vector< Vector3D > &vector_field)
Definition symmetry.hpp:634
static KPointsGrid reduce_kpoints_internal(const Crystal &crystal, const KPointsGrid &input_grid, bool use_time_reversal=true, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:262
static std::vector< SymmetryOperation > get_symmetry_operations(const Crystal &crystal, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:484
static int number_of_symmetry_operations(const Crystal &crystal, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Get the number of symmetry operations.
Definition symmetry.hpp:105
static KPointsGrid reduce_kpoints(const Crystal &crystal, const KPointsGrid &input_grid, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:131
static void symmetrize_matrix(const Crystal &crystal, const std::vector< SymmetryOperation > &symmetry_operations, Matrix3x3 &tensor)
Definition symmetry.hpp:740
void crystal_to_spglib(const Crystal &crystal, double lattice[3][3], std::vector< double > &positions, std::vector< int > &types)
Definition symmetry.hpp:30
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
constexpr double zero_vector_magnitude_threshold
constexpr double default_symmetry_precision
double Real
3×3 matrix stored row-major: row i = mat[i]
Vector3D coordinates
std::array< std::array< int, 3 >, 3 > rotation
Definition operation.hpp:27
Vector3D fractional_translation
Definition operation.hpp:29
std::array< std::array< int, 3 >, 3 > reciprocal_rotation
Definition operation.hpp:28