Source code for quscope.quantum_ctem.workflows.graphene

"""
Graphene Quantum CTEM Workflow.

Complete workflow for quantum simulation of graphene CTEM images,
optimized for weak phase object validation and honeycomb lattice imaging.
"""

from typing import Dict, List, Optional, Tuple

import numpy as np

from ..backends.base import Backend
from ..materials import Graphene
from .base import CTEMWorkflow, MicroscopeConfig, SimulationResult


[docs] class GrapheneWorkflow(CTEMWorkflow): """ Quantum CTEM workflow for Graphene. Graphene is ideal for quantum CTEM simulation validation because: - Light atoms (C, Z=6) give excellent WPOA validity - Honeycomb lattice provides clear symmetry tests - Well-characterized diffraction pattern for validation - Single-atom thickness eliminates multislice complexity Examples: >>> from quscope.quantum_ctem.backends import get_backend >>> from quscope.quantum_ctem.workflows import GrapheneWorkflow >>> # Quick simulation with simulator >>> backend = get_backend("simulator") >>> workflow = GrapheneWorkflow(backend=backend) >>> result = workflow.run(nx=5, ny=5, grid_size=64) >>> print(result.summary()) >>> # With IBM hardware >>> backend = get_backend("ibm", device_name="ibm_kyoto") >>> workflow = GrapheneWorkflow(backend=backend, voltage=80e3) >>> result = workflow.run(nx=5, ny=5, grid_size=64, shots=4096) >>> # Nanoribbon simulation >>> result = workflow.run_nanoribbon(width=10, length=20, edge_type="zigzag") """ def __init__( self, backend: Backend, voltage: float = 80e3, # Lower voltage better for graphene defocus: Optional[float] = None, cs: float = 0.001, # Aberration-corrected for graphene edge_type: str = "zigzag", **kwargs, ): """ Initialize Graphene workflow. Args: backend: Quantum backend (simulator or IBM) voltage: Accelerating voltage in V (default: 80 kV for graphene) defocus: Defocus in Å (default: Scherzer defocus) cs: Spherical aberration in mm (default: near-zero, corrected) edge_type: Edge type for nanoribbons ("zigzag" or "armchair") """ material = Graphene(edge_type=edge_type) # Setup microscope - lower voltage, aberration-corrected for graphene microscope = MicroscopeConfig(voltage=voltage, cs=cs) if defocus is None: defocus = microscope.scherzer_defocus microscope.defocus = defocus super().__init__(material, backend, microscope, **kwargs) self.edge_type = edge_type
[docs] def build_structure( self, nx: int = 5, ny: int = 5, vacuum: float = 10.0, **kwargs, ): """ Build graphene supercell. Args: nx: Unit cells in x direction ny: Unit cells in y direction vacuum: Vacuum padding in Å Returns: ASE Atoms object """ return self.material.build_structure(nx=nx, ny=ny, vacuum=vacuum)
[docs] def run( self, nx: int = 5, ny: int = 5, grid_size: int = 64, pixel_size: float = 0.05, # Finer for graphene shots: int = 0, apply_ctf: bool = True, compare_classical: bool = False, vacuum: float = 10.0, ) -> SimulationResult: """ Run graphene quantum CTEM simulation. Args: nx: Unit cells in x ny: Unit cells in y grid_size: Grid size (must be power of 2) pixel_size: Pixel size in Å (default finer for graphene) shots: Measurement shots (0 for statevector) apply_ctf: Apply contrast transfer function compare_classical: Run classical comparison vacuum: Vacuum padding in Å Returns: SimulationResult with complete simulation data """ result = super().run( grid_size=grid_size, pixel_size=pixel_size, shots=shots, apply_ctf=apply_ctf, compare_classical=compare_classical, nx=nx, ny=ny, vacuum=vacuum, ) # Update supercell size in result result.supercell_size = (nx, ny) # Add WPOA validity check validity = self.material.wpoa_validity(self.microscope.voltage) result.wpoa_valid = validity["wpoa_valid"] result.max_phase_shift = validity["max_phase_shift"] return result
[docs] def run_nanoribbon( self, width: int = 10, length: int = 20, edge_type: Optional[str] = None, grid_size: int = 64, pixel_size: float = 0.05, shots: int = 0, apply_ctf: bool = True, saturated: bool = False, ) -> SimulationResult: """ Run quantum CTEM simulation of graphene nanoribbon. Args: width: Ribbon width in unit cells length: Ribbon length in unit cells edge_type: "zigzag" or "armchair" (default: instance setting) grid_size: Grid size pixel_size: Pixel size in Å shots: Measurement shots apply_ctf: Apply CTF saturated: Saturate edges with hydrogen Returns: SimulationResult for nanoribbon """ import time start_time = time.time() # Build nanoribbon edge = edge_type or self.edge_type atoms = self.material.build_nanoribbon( width=width, length=length, edge_type=edge, saturated=saturated, ) # Setup quantum state V_proj, transmission = self.setup_quantum_state( atoms, grid_size=grid_size, pixel_size=pixel_size ) # Build and run circuit circuit = self.build_quantum_circuit(transmission, apply_ctf=apply_ctf) from ..backends.base import BackendConfig config = BackendConfig(shots=shots) backend_result = self.backend.run(circuit, config) # Extract results if backend_result.statevector is not None: wavefunction = backend_result.get_statevector_2d(grid_size, grid_size) intensity = np.abs(wavefunction) ** 2 phase = np.angle(wavefunction) else: wavefunction = None intensity = None phase = None cell = atoms.get_cell() fov = (cell[0, 0], cell[1, 1]) result = SimulationResult( material_name=f"Graphene Nanoribbon ({edge})", n_atoms=len(atoms), supercell_size=(width, length), grid_size=grid_size, pixel_size=pixel_size, field_of_view=fov, wavefunction=wavefunction, intensity=intensity, phase=phase, projected_potential=V_proj, transmission_function=transmission, microscope_config=self.microscope, backend_result=backend_result, execution_time=time.time() - start_time, circuit_depth=circuit.depth(), n_qubits=circuit.num_qubits, ) return result
[docs] def run_with_vacancies( self, nx: int = 10, ny: int = 10, vacancy_fraction: float = 0.02, grid_size: int = 64, pixel_size: float = 0.05, shots: int = 0, seed: Optional[int] = None, ) -> SimulationResult: """ Run simulation of graphene with vacancy defects. Args: nx, ny: Supercell size vacancy_fraction: Fraction of atoms to remove (0-1) grid_size: Grid size pixel_size: Pixel size in Å shots: Measurement shots seed: Random seed for vacancy positions Returns: SimulationResult showing vacancy contrast """ import time start_time = time.time() # Build defective graphene atoms = self.material.build_with_vacancy( nx=nx, ny=ny, vacancy_fraction=vacancy_fraction, seed=seed, ) # Setup and run V_proj, transmission = self.setup_quantum_state( atoms, grid_size=grid_size, pixel_size=pixel_size ) circuit = self.build_quantum_circuit(transmission, apply_ctf=True) from ..backends.base import BackendConfig config = BackendConfig(shots=shots) backend_result = self.backend.run(circuit, config) if backend_result.statevector is not None: wavefunction = backend_result.get_statevector_2d(grid_size, grid_size) intensity = np.abs(wavefunction) ** 2 phase = np.angle(wavefunction) else: wavefunction = None intensity = None phase = None cell = atoms.get_cell() fov = (cell[0, 0], cell[1, 1]) result = SimulationResult( material_name=f"Graphene with {vacancy_fraction*100:.1f}% vacancies", n_atoms=len(atoms), supercell_size=(nx, ny), grid_size=grid_size, pixel_size=pixel_size, field_of_view=fov, wavefunction=wavefunction, intensity=intensity, phase=phase, projected_potential=V_proj, transmission_function=transmission, microscope_config=self.microscope, backend_result=backend_result, execution_time=time.time() - start_time, circuit_depth=circuit.depth(), n_qubits=circuit.num_qubits, ) return result
[docs] def validate_wpoa(self) -> Dict[str, any]: """ Validate WPOA approximation for current settings. Returns: Dictionary with validity metrics """ return self.material.wpoa_validity(self.microscope.voltage)
[docs] def visualize( self, result: SimulationResult, show_potential: bool = True, show_intensity: bool = True, show_phase: bool = True, show_fft: bool = True, figsize: Tuple[int, int] = (14, 4), save_path: Optional[str] = None, ): """ Visualize graphene simulation results. Args: result: SimulationResult to visualize show_potential: Show projected potential show_intensity: Show image intensity show_phase: Show phase map show_fft: Show FFT (diffraction pattern) figsize: Figure size save_path: Path to save figure Returns: matplotlib Figure """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("matplotlib required") n_plots = sum([show_potential, show_intensity, show_phase, show_fft]) fig, axes = plt.subplots(1, n_plots, figsize=figsize) if n_plots == 1: axes = [axes] idx = 0 extent = [0, result.field_of_view[0], 0, result.field_of_view[1]] if show_potential and result.projected_potential is not None: ax = axes[idx] im = ax.imshow(result.projected_potential, cmap="viridis", extent=extent) ax.set_title("Projected Potential V(x,y)") ax.set_xlabel("x (Å)") ax.set_ylabel("y (Å)") plt.colorbar(im, ax=ax, label="V·Å") idx += 1 if show_intensity and result.intensity is not None: ax = axes[idx] im = ax.imshow(result.intensity, cmap="gray", extent=extent) ax.set_title(f"CTEM Image (Δf={result.microscope_config.defocus:.0f} Å)") ax.set_xlabel("x (Å)") ax.set_ylabel("y (Å)") plt.colorbar(im, ax=ax, label="Intensity") idx += 1 if show_phase and result.phase is not None: ax = axes[idx] im = ax.imshow( result.phase, cmap="twilight", extent=extent, vmin=-np.pi, vmax=np.pi, ) ax.set_title("Phase φ(x,y)") ax.set_xlabel("x (Å)") ax.set_ylabel("y (Å)") plt.colorbar(im, ax=ax, label="Phase (rad)") idx += 1 if show_fft and result.wavefunction is not None: ax = axes[idx] fft = np.fft.fftshift(np.fft.fft2(result.wavefunction)) fft_intensity = np.abs(fft) ** 2 # Log scale for better visibility fft_log = np.log10(fft_intensity + 1) im = ax.imshow(fft_log, cmap="hot") ax.set_title("Diffraction Pattern (log)") ax.set_xlabel("kx") ax.set_ylabel("ky") plt.colorbar(im, ax=ax) idx += 1 plt.suptitle( f"Graphene Quantum CTEM Simulation " f"({result.supercell_size[0]}×{result.supercell_size[1]} supercell, " f"{result.microscope_config.voltage/1e3:.0f} kV)", fontsize=12, ) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=150, bbox_inches="tight") return fig
[docs] def visualize_honeycomb( self, result: SimulationResult, zoom_factor: float = 2.0, figsize: Tuple[int, int] = (10, 5), save_path: Optional[str] = None, ): """ Specialized visualization highlighting honeycomb lattice. Args: result: SimulationResult zoom_factor: Zoom into center region figsize: Figure size save_path: Path to save figure """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("matplotlib required") fig, axes = plt.subplots(1, 2, figsize=figsize) # Full view extent = [0, result.field_of_view[0], 0, result.field_of_view[1]] axes[0].imshow(result.intensity, cmap="gray", extent=extent) axes[0].set_title("Full View") axes[0].set_xlabel("x (Å)") axes[0].set_ylabel("y (Å)") # Zoomed center ny, nx = result.intensity.shape cx, cy = nx // 2, ny // 2 zoom_size = int(nx / (2 * zoom_factor)) zoomed = result.intensity[ cy - zoom_size : cy + zoom_size, cx - zoom_size : cx + zoom_size ] zoom_extent = [ result.field_of_view[0] / 2 - result.field_of_view[0] / (2 * zoom_factor), result.field_of_view[0] / 2 + result.field_of_view[0] / (2 * zoom_factor), result.field_of_view[1] / 2 - result.field_of_view[1] / (2 * zoom_factor), result.field_of_view[1] / 2 + result.field_of_view[1] / (2 * zoom_factor), ] axes[1].imshow(zoomed, cmap="gray", extent=zoom_extent) axes[1].set_title(f"Honeycomb Detail ({zoom_factor}x zoom)") axes[1].set_xlabel("x (Å)") plt.suptitle( f"Graphene Honeycomb Lattice at {result.microscope_config.voltage/1e3:.0f} kV" ) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=150, bbox_inches="tight") return fig