Skip to content

Commit d7db321

Browse files
committed
Add get_primitive_cell() method to Structure class
This commit adds a new method to extract the primitive cell from a supercell or conventional cell using spglib symmetries. Features: - get_primitive_cell() method in Structure class - Uses spglib.standardize_cell() to find primitive cell - Supports return_itau parameter for atom mapping - Exposes symprec, angle_tolerance, and no_idealize options - Returns copy for already-primitive cells - Preserves masses dictionary Includes: - Comprehensive implementation in cellconstructor/Structure.py - Test suite in tests/TestPrimitiveCell/test_get_primitive_cell.py - User documentation in UserGuide/gettingstarted.rst - Sphinx config fixes in UserGuide/conf.py
1 parent e5f5fff commit d7db321

4 files changed

Lines changed: 389 additions & 4 deletions

File tree

UserGuide/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
#
8181
# This is also used if you do content translation via gettext catalogs.
8282
# Usually you set "language" from the command line for these cases.
83-
language = None
83+
language = 'en'
8484

8585
# List of patterns, relative to source directory, that match files and
8686
# directories to ignore when looking for source files.
@@ -107,7 +107,7 @@
107107
# Add any paths that contain custom static files (such as style sheets) here,
108108
# relative to this directory. They are copied after the builtin static files,
109109
# so a file named "default.css" will overwrite the builtin "default.css".
110-
html_static_path = ['_static']
110+
# html_static_path = ['_static']
111111

112112
# Custom sidebar templates, must be a dictionary that maps document names
113113
# to template names.
@@ -200,7 +200,7 @@
200200
# -- Options for intersphinx extension ---------------------------------------
201201

202202
# Example configuration for intersphinx: refer to the Python standard library.
203-
intersphinx_mapping = {'https://docs.python.org/': None}
203+
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
204204

205205
# -- Options for todo extension ----------------------------------------------
206206

UserGuide/gettingstarted.rst

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,80 @@ Vice-versa, you can generate an ASE Atoms structure using the function get_ase_a
500500
:members: get_ase_atoms
501501

502502

503-
503+
Extract the primitive cell
504+
--------------------------
505+
506+
When working with crystal structures, you often need to find the primitive cell from a conventional cell or supercell.
507+
CellConstructor provides the ``get_primitive_cell()`` method that uses spglib to identify the smallest repeating unit
508+
by exploiting the crystal symmetries.
509+
510+
This is particularly useful when:
511+
512+
- You have a conventional cell and want to reduce it to the primitive cell for computational efficiency
513+
- You need to identify the irreducible atoms in the unit cell
514+
- You want to standardize the cell representation
515+
516+
Here is an example of how to extract the primitive cell from a conventional FCC cell:
517+
518+
.. code:: python
519+
520+
import cellconstructor as CC
521+
import cellconstructor.Structure
522+
import numpy as np
523+
524+
# Create a conventional FCC cell (4 atoms)
525+
fcc_conv = CC.Structure.Structure()
526+
fcc_conv.unit_cell = np.array([[4, 0, 0], [0, 4, 0], [0, 0, 4]], dtype=np.float64)
527+
fcc_conv.has_unit_cell = True
528+
fcc_conv.atoms = ["Al", "Al", "Al", "Al"]
529+
fcc_conv.N_atoms = 4
530+
fcc_conv.coords = np.array([
531+
[0, 0, 0],
532+
[0, 2, 2],
533+
[2, 0, 2],
534+
[2, 2, 0]
535+
], dtype=np.float64)
536+
fcc_conv.masses = {"Al": 26.98}
537+
538+
# Extract the primitive cell
539+
fcc_prim = fcc_conv.get_primitive_cell()
540+
541+
print("Conventional cell atoms:", fcc_conv.N_atoms)
542+
print("Primitive cell atoms:", fcc_prim.N_atoms)
543+
print("Primitive cell lattice:")
544+
print(fcc_prim.unit_cell)
545+
546+
The method returns a new Structure object containing the primitive cell with:
547+
548+
- Reduced number of atoms (1 atom for FCC primitive instead of 4 in conventional)
549+
- Primitive lattice vectors
550+
- Atomic positions in Cartesian coordinates
551+
- Preserved masses dictionary
552+
553+
You can also obtain the mapping between the original atoms and the primitive atoms using the ``return_itau`` parameter:
554+
555+
.. code:: python
556+
557+
# Get the primitive cell with the itau mapping
558+
fcc_prim, itau = fcc_conv.get_primitive_cell(return_itau=True)
559+
560+
# itau[i] gives the index of the primitive atom that corresponds to original atom i
561+
print("itau mapping:", itau)
562+
# Output: [0 0 0 0] - all 4 atoms map to the single primitive atom
563+
564+
Additional parameters allow fine control over the primitive cell search:
565+
566+
- ``symprec``: Symmetry precision tolerance (default: 1e-5)
567+
- ``angle_tolerance``: Tolerance for angles between basis vectors in degrees (default: -1, disabled)
568+
- ``no_idealize``: If True, disable idealization of basis vectors (default: False)
569+
570+
If the input structure is already a primitive cell, the method returns a copy of the structure.
571+
572+
573+
.. autoclass:: Structures.Structures
574+
:members: get_primitive_cell
575+
576+
504577
Load and save the dynamical matrix
505578
----------------------------------
506579

cellconstructor/Structure.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,130 @@ def get_spglib_cell(self):
14991499
cell = (lattice, positions, numbers)
15001500
return cell
15011501

1502+
def get_primitive_cell(self, symprec=1e-5, angle_tolerance=-1.0, no_idealize=False,
1503+
return_itau=False):
1504+
"""
1505+
GET PRIMITIVE CELL
1506+
==================
1507+
1508+
This method uses spglib to find the primitive cell from a supercell structure.
1509+
It exploits the crystal symmetries to identify the smallest repeating unit.
1510+
1511+
If the input structure is already a primitive cell, a copy is returned.
1512+
1513+
Parameters
1514+
----------
1515+
symprec : float, optional
1516+
Symmetry precision tolerance in Cartesian coordinates.
1517+
Default is 1e-5.
1518+
angle_tolerance : float, optional
1519+
Tolerance of angle between basis vectors in degrees.
1520+
If negative (default), the angle tolerance is disabled.
1521+
no_idealize : bool, optional
1522+
If True, disable idealization of basis vectors and atomic positions.
1523+
This preserves the original Cartesian coordinates. Default is False.
1524+
return_itau : bool, optional
1525+
If True, also return the itau array mapping from input atoms to primitive atoms.
1526+
Similar to the itau in generate_supercell, this indicates which primitive atom
1527+
each input atom corresponds to. Default is False.
1528+
1529+
Returns
1530+
-------
1531+
Structure or tuple
1532+
The primitive cell structure. If return_itau is True, returns a tuple
1533+
(primitive_structure, itau) where itau is an ndarray indicating
1534+
which primitive atom each input atom corresponds to.
1535+
1536+
Raises
1537+
------
1538+
ImportError
1539+
If spglib is not available.
1540+
ValueError
1541+
If the structure has no unit cell or if primitive search fails.
1542+
1543+
Notes
1544+
-----
1545+
This method requires spglib to be installed. The optional dependency is
1546+
handled with the __SPGLIB__ flag from the symmetries module.
1547+
1548+
The primitive cell search may change the orientation of the lattice vectors
1549+
to match spglib's standard conventions, unless no_idealize=True.
1550+
1551+
The masses dictionary is copied from the original structure to the primitive
1552+
structure, preserving all atomic masses.
1553+
"""
1554+
# Check if spglib is available
1555+
if not SYM.__SPGLIB__:
1556+
raise ImportError("Error, get_primitive_cell requires spglib to be installed.")
1557+
1558+
# Check if structure has unit cell
1559+
if not self.has_unit_cell:
1560+
raise ValueError("Error, the structure must have a valid unit cell to find the primitive cell.")
1561+
1562+
# Get the spglib cell representation
1563+
cell = self.get_spglib_cell()
1564+
1565+
# Use spglib to find the primitive cell
1566+
# standardize_cell with to_primitive=True is the modern recommended approach
1567+
primitive_cell = SYM.spglib.standardize_cell(
1568+
cell,
1569+
to_primitive=True,
1570+
no_idealize=no_idealize,
1571+
symprec=symprec,
1572+
angle_tolerance=angle_tolerance
1573+
)
1574+
1575+
if primitive_cell is None:
1576+
raise ValueError("Error, spglib failed to find the primitive cell. "
1577+
"The structure may be too distorted or symprec too strict.")
1578+
1579+
# Unpack the primitive cell data
1580+
primitive_lattice, primitive_positions, primitive_numbers = primitive_cell
1581+
1582+
# Create a new Structure for the primitive cell
1583+
primitive_struct = Structure()
1584+
primitive_struct.has_unit_cell = True
1585+
primitive_struct.unit_cell = np.array(primitive_lattice, dtype=np.float64)
1586+
1587+
# Convert fractional positions to Cartesian coordinates
1588+
# primitive_positions are in fractional coordinates of the primitive lattice
1589+
primitive_struct.coords = np.zeros((len(primitive_positions), 3), dtype=np.float64)
1590+
for i, pos in enumerate(primitive_positions):
1591+
# Convert fractional to Cartesian: r_cart = r_frac * lattice_vectors
1592+
primitive_struct.coords[i, :] = np.dot(pos, primitive_lattice)
1593+
1594+
primitive_struct.N_atoms = len(primitive_numbers)
1595+
1596+
# Map atomic numbers back to symbols
1597+
# We need to reverse the mapping from get_spglib_cell()
1598+
# Build the forward mapping first (same logic as get_spglib_cell)
1599+
forward_mapping = {}
1600+
for s in self.atoms:
1601+
forward_mapping.setdefault(s, len(forward_mapping) + 1)
1602+
1603+
# Create reverse mapping: number -> symbol
1604+
reverse_mapping = {v: k for k, v in forward_mapping.items()}
1605+
1606+
# Assign atomic symbols
1607+
primitive_struct.atoms = [reverse_mapping[num] for num in primitive_numbers]
1608+
1609+
# Copy the masses dictionary from the original structure
1610+
primitive_struct.masses = self.masses.copy()
1611+
1612+
# Handle return_itau
1613+
if return_itau:
1614+
# Get the symmetry dataset which contains the mapping information
1615+
dataset = SYM.spglib.get_symmetry_dataset(cell, symprec=symprec, angle_tolerance=angle_tolerance)
1616+
if dataset is None:
1617+
raise ValueError("Error, spglib failed to get symmetry dataset for itau mapping.")
1618+
1619+
# mapping_to_primitive gives the index of the primitive atom for each input atom
1620+
# This is analogous to itau in generate_supercell
1621+
itau = np.array(dataset.mapping_to_primitive, dtype=np.intc)
1622+
return primitive_struct, itau
1623+
1624+
return primitive_struct
1625+
15021626
def get_phonopy_calculation(self, supercell = [1,1,1]):
15031627
"""
15041628
Convert the CellConstructor structure to a phonopy object

0 commit comments

Comments
 (0)