Source code for quscope.quantum_ctem.materials.graphene

"""
Graphene Material Definition.

Graphene is a 2D carbon allotrope with a honeycomb lattice structure.
As a weak phase object with light atoms (Z=6), it's ideal for testing
WPOA validity and quantum simulation accuracy.
"""

from typing import Dict, Optional

import numpy as np

from .base import AtomicScatteringParams, Material, MaterialParameters


[docs] class Graphene(Material): """ Graphene material for CTEM simulation. Graphene has: - Honeycomb lattice with a ≈ 2.46 Å - Single atomic layer of carbon (Z=6) - Excellent WPOA validity due to weak scattering - Six-fold symmetry in diffraction pattern Attributes: edge_type: "zigzag" or "armchair" for nanoribbons Examples: >>> graphene = Graphene() >>> atoms = graphene.build_structure(nx=5, ny=5) >>> V = graphene.get_projected_potential(atoms, grid_size=256) >>> # Build nanoribbon >>> ribbon = graphene.build_nanoribbon(width=10, length=50) """ # Kirkland scattering parameters for Carbon # From Kirkland, "Advanced Computing in Electron Microscopy", Appendix C SCATTERING_PARAMS = { "C": AtomicScatteringParams( symbol="C", atomic_number=6, a_coefficients=[0.7307, 0.6166, 0.2098, 0.1058], b_coefficients=[0.0207, 0.1813, 0.7028, 2.8454], ), } # Graphene lattice parameters LATTICE_CONSTANT = 2.46 # Å (in-plane) BOND_LENGTH = 1.42 # Å (C-C bond) LAYER_SPACING = 3.35 # Å (for multilayer) def __init__(self, edge_type: str = "zigzag"): """ Initialize Graphene material. Args: edge_type: "zigzag" or "armchair" (affects nanoribbon edges) """ super().__init__() self.edge_type = edge_type self._parameters = MaterialParameters( name="Graphene", formula="C", lattice_constants=(2.46, 2.46, 3.35), # Å lattice_angles=(90.0, 90.0, 120.0), space_group="P6/mmm", elements=["C"], typical_thickness=3.35, # Single layer ) @property def parameters(self) -> MaterialParameters: return self._parameters
[docs] def build_structure( self, nx: int = 5, ny: int = 5, vacuum: float = 10.0, **kwargs, ): """ Build graphene supercell using ASE. Args: nx: Number of unit cells in x direction ny: Number of unit cells in y direction vacuum: Vacuum padding in z direction (Å) Returns: ASE Atoms object """ try: from ase.build import graphene as ase_graphene except ImportError: raise ImportError( "ASE is required for structure building. " "Install with: pip install ase" ) # Build graphene using ASE atoms = ase_graphene( a=self.LATTICE_CONSTANT, size=(nx, ny, 1), vacuum=vacuum, ) atoms.center() return atoms
[docs] def build_supercell( self, nx: int = 5, ny: int = 5, vacuum: float = 10.0, ): """Alias for build_structure for API consistency.""" return self.build_structure(nx=nx, ny=ny, vacuum=vacuum)
[docs] def build_nanoribbon( self, width: int = 10, length: int = 20, edge_type: Optional[str] = None, vacuum: float = 10.0, saturated: bool = False, ): """ Build graphene nanoribbon. Args: width: Width in unit cells (perpendicular to ribbon axis) length: Length in unit cells (along ribbon axis) edge_type: "zigzag" or "armchair" (defaults to instance setting) vacuum: Vacuum padding (Å) saturated: If True, saturate edges with hydrogen Returns: ASE Atoms object """ try: from ase.build import graphene_nanoribbon except ImportError: raise ImportError( "ASE is required for structure building. " "Install with: pip install ase" ) edge = edge_type or self.edge_type if edge not in ["zigzag", "armchair"]: raise ValueError(f"edge_type must be 'zigzag' or 'armchair', got {edge}") atoms = graphene_nanoribbon( n=width, m=length, type=edge, saturated=saturated, vacuum=vacuum, ) atoms.center() return atoms
[docs] def build_with_vacancy( self, nx: int = 5, ny: int = 5, vacancy_fraction: float = 0.01, vacuum: float = 10.0, seed: Optional[int] = None, ): """ Build graphene with random vacancies. Args: nx, ny: Supercell size vacancy_fraction: Fraction of atoms to remove (0-1) vacuum: Vacuum padding (Å) seed: Random seed for reproducibility Returns: ASE Atoms object with vacancies """ atoms = self.build_structure(nx=nx, ny=ny, vacuum=vacuum) if seed is not None: np.random.seed(seed) n_atoms = len(atoms) n_remove = int(n_atoms * vacancy_fraction) if n_remove > 0: remove_indices = np.random.choice(n_atoms, n_remove, replace=False) # Remove atoms in reverse order to preserve indices for idx in sorted(remove_indices, reverse=True): del atoms[idx] return atoms
[docs] def get_scattering_params(self) -> Dict[str, AtomicScatteringParams]: """Get Kirkland scattering parameters for Carbon.""" return self.SCATTERING_PARAMS.copy()
[docs] def get_sublattice_positions(self, atoms) -> Dict[str, np.ndarray]: """ Get positions of A and B sublattice atoms. In graphene, the honeycomb lattice has two sublattices. This is useful for analyzing sublattice-resolved contrast. Args: atoms: ASE Atoms object Returns: Dictionary with "A" and "B" sublattice positions """ positions = atoms.get_positions()[:, :2] # Determine sublattice based on fractional coordinates cell = atoms.get_cell()[:2, :2] # Simple heuristic: classify by y-coordinate modulo unit cell a = self.LATTICE_CONSTANT y_mod = positions[:, 1] % (a * np.sqrt(3) / 2) threshold = a * np.sqrt(3) / 4 a_mask = y_mod < threshold b_mask = ~a_mask return { "A": positions[a_mask], "B": positions[b_mask], }
[docs] def wpoa_validity(self, voltage: float = 200e3) -> Dict[str, float]: """ Assess WPOA validity for graphene at given voltage. The WPOA is valid when σ·V_proj << 1. Args: voltage: Accelerating voltage in V Returns: Dictionary with validity metrics """ sigma = self.get_interaction_constant(voltage) # Approximate maximum projected potential for C atom # Using peak value from Kirkland parameterization V_max_approx = sum(self.SCATTERING_PARAMS["C"].a_coefficients) phase_shift = sigma * V_max_approx return { "interaction_constant": sigma, "max_projected_potential": V_max_approx, "max_phase_shift": phase_shift, "wpoa_valid": phase_shift < 0.3, # Rule of thumb: < 0.3 rad "validity_ratio": phase_shift / 0.3, }
[docs] def expected_contrast(self, voltage: float = 200e3) -> str: """ Describe expected CTEM contrast for graphene. Args: voltage: Accelerating voltage in V Returns: Description of expected contrast """ validity = self.wpoa_validity(voltage) valid_str = "valid" if validity["wpoa_valid"] else "marginally valid" return ( f"At {voltage/1e3:.0f} kV:\n" f"- WPOA is {valid_str} (phase shift: {validity['max_phase_shift']:.3f} rad)\n" "- Carbon atoms appear as weak spots in honeycomb pattern\n" "- Six-fold symmetry in diffraction\n" "- Very weak contrast compared to heavy elements\n" "- Ideal for validating quantum simulation accuracy" )
[docs] def get_diffraction_spots(self) -> Dict[str, np.ndarray]: """ Get expected diffraction spot positions for graphene. Returns: Dictionary with reciprocal lattice vectors and spot positions """ a = self.LATTICE_CONSTANT # Reciprocal lattice vectors for hexagonal lattice b1 = 2 * np.pi / a * np.array([1, 1 / np.sqrt(3)]) b2 = 2 * np.pi / a * np.array([0, 2 / np.sqrt(3)]) return { "b1": b1, "b2": b2, "first_order": [b1, b2, b1 + b2, -b1, -b2, -(b1 + b2)], "lattice_constant": a, }