Source code for sim.pulses.sequences

# ==============================================================================
#  QUSIM - Quantum Simulator for NV Centers
#  Leon Kaiser, MSQC Goethe University, Frankfurt, Germany
#  https://msqc.cgi-host6.rz.uni-frankfurt.de
#  I.kaiser[at]em.uni-frankfurt.de
#
#  This software is provided for scientific and educational purposes.
#  Free to use, modify, and distribute with attribution.
# ==============================================================================
"""
Standard pulse sequences for NV center experiments.

Implements common sequences for coherence measurements and
dynamical decoupling.

Sequence Types
--------------
Ramsey: Free induction decay (T2*)
    π/2 - τ - π/2

Spin Echo: Hahn echo (T2)
    π/2 - τ/2 - π - τ/2 - π/2

CPMG: Carr-Purcell-Meiboom-Gill (extended coherence)
    π/2 - [τ/2 - π_Y - τ/2]^N - π/2

XY4/XY8: Dynamical decoupling (robust to pulse errors)
    π/2 - [τ - π_X - τ - π_Y - τ - π_X - τ - π_Y]^N - π/2
"""

import numpy as np
from dataclasses import dataclass, field
from typing import List, Optional

from .basic import Pulse, pi_pulse, pi_half_pulse


[docs] @dataclass class PulseSequence: """ Collection of pulses with timing. Attributes ---------- pulses : list of Pulse List of pulses in the sequence delays : list of float Delay after each pulse in seconds Examples -------- >>> seq = PulseSequence() >>> seq.add_pulse(pi_half_pulse(10), delay=1e-6) >>> seq.add_pulse(pi_pulse(10), delay=1e-6) >>> seq.add_pulse(pi_half_pulse(10)) >>> print(f"Total duration: {seq.total_duration()*1e6:.1f} μs") """ pulses: List[Pulse] = field(default_factory=list) delays: List[float] = field(default_factory=list)
[docs] def add_pulse(self, pulse: Pulse, delay: float = 0.0): """ Add a pulse to the sequence. Parameters ---------- pulse : Pulse Pulse to add delay : float Delay after the pulse in seconds. Default: 0 """ self.pulses.append(pulse) self.delays.append(delay)
[docs] def add_delay(self, delay: float): """Add a delay without a pulse.""" # This extends the last delay if self.delays: self.delays[-1] += delay
[docs] def total_duration(self) -> float: """Calculate total sequence duration.""" t = 0.0 for pulse, delay in zip(self.pulses, self.delays): t += pulse.duration + delay return t
[docs] def to_awg(self, awg): """ Load the sequence into an AWG interface. Parameters ---------- awg : AWGInterface AWG interface to load into """ for pulse, delay in zip(self.pulses, self.delays): awg.add_pulse( shape=pulse.shape, amplitude=pulse.amplitude, duration=pulse.duration, phase=pulse.phase, **pulse.kwargs ) if delay > 0: awg.add_delay(delay)
def __repr__(self) -> str: n = len(self.pulses) t = self.total_duration() return f"PulseSequence({n} pulses, {t*1e9:.1f} ns)"
[docs] def ramsey_sequence( tau: float, rabi_freq_mhz: float = 1.0, final_phase: float = 0.0, shape: str = "rect" ) -> PulseSequence: """ Create a Ramsey sequence: π/2 - τ - π/2. Parameters ---------- tau : float Free evolution time in seconds rabi_freq_mhz : float Rabi frequency for the pulses. Default: 1 MHz final_phase : float Phase of the final π/2 pulse in radians. Default: 0 shape : str Pulse shape. Default: 'rect' Returns ------- PulseSequence Ramsey sequence Examples -------- >>> seq = ramsey_sequence(tau=1e-6, rabi_freq_mhz=10) >>> print(seq.total_duration()) """ seq = PulseSequence() # First π/2 pulse (X axis) seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) # Second π/2 pulse (variable phase) final_pulse = pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape) final_pulse.phase = final_phase seq.add_pulse(final_pulse) return seq
[docs] def spin_echo_sequence( tau: float, rabi_freq_mhz: float = 1.0, shape: str = "rect" ) -> PulseSequence: """ Create a spin echo (Hahn echo) sequence: π/2 - τ/2 - π - τ/2 - π/2. Parameters ---------- tau : float Total free evolution time in seconds rabi_freq_mhz : float Rabi frequency for the pulses. Default: 1 MHz shape : str Pulse shape. Default: 'rect' Returns ------- PulseSequence Spin echo sequence """ seq = PulseSequence() half_tau = tau / 2 # π/2_X seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=half_tau) # π_Y (refocusing) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=half_tau) # π/2_X (readout) seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape)) return seq
[docs] def cpmg_sequence( tau: float, n_pulses: int, rabi_freq_mhz: float = 1.0, shape: str = "rect" ) -> PulseSequence: """ Create a CPMG sequence: π/2 - [τ - π_Y - τ]^N - π/2. Parameters ---------- tau : float Delay between refocusing pulses in seconds n_pulses : int Number of refocusing π pulses rabi_freq_mhz : float Rabi frequency. Default: 1 MHz shape : str Pulse shape. Default: 'rect' Returns ------- PulseSequence CPMG sequence """ seq = PulseSequence() half_tau = tau / 2 # Initial π/2_X seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=half_tau) # N refocusing pulses for i in range(n_pulses): delay = tau if i < n_pulses - 1 else half_tau seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=delay) # Final π/2_X seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape)) return seq
[docs] def xy4_sequence( tau: float, n_cycles: int = 1, rabi_freq_mhz: float = 1.0, shape: str = "rect" ) -> PulseSequence: """ Create an XY4 dynamical decoupling sequence. Structure: π/2 - [τ - π_X - τ - π_Y - τ - π_X - τ - π_Y]^N - π/2 Parameters ---------- tau : float Delay between pulses in seconds n_cycles : int Number of XY4 cycles. Default: 1 rabi_freq_mhz : float Rabi frequency. Default: 1 MHz shape : str Pulse shape. Default: 'rect' Returns ------- PulseSequence XY4 sequence Notes ----- XY4 is robust to pulse flip-angle errors due to the alternating X and Y phases. """ seq = PulseSequence() # Initial π/2_X seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) # XY4 cycles: X - Y - X - Y for _ in range(n_cycles): seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) # Final π/2_X # Adjust last delay if seq.delays: seq.delays[-1] = 0 # Remove last delay before final pulse seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape)) return seq
[docs] def xy8_sequence( tau: float, n_cycles: int = 1, rabi_freq_mhz: float = 1.0, shape: str = "rect" ) -> PulseSequence: """ Create an XY8 dynamical decoupling sequence. Structure: π/2 - [τ - π_X - τ - π_Y - τ - π_X - τ - π_Y - τ - π_Y - τ - π_X - τ - π_Y - τ - π_X]^N - π/2 Parameters ---------- tau : float Delay between pulses in seconds n_cycles : int Number of XY8 cycles. Default: 1 rabi_freq_mhz : float Rabi frequency. Default: 1 MHz shape : str Pulse shape. Default: 'rect' Returns ------- PulseSequence XY8 sequence Notes ----- XY8 is more robust than XY4 due to the symmetric phase pattern. """ seq = PulseSequence() # Initial π/2_X seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) # XY8 cycles: X Y X Y Y X Y X for _ in range(n_cycles): seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='y', shape=shape), delay=tau) seq.add_pulse(pi_pulse(rabi_freq_mhz, axis='x', shape=shape), delay=tau) # Adjust last delay and add final π/2 if seq.delays: seq.delays[-1] = 0 seq.add_pulse(pi_half_pulse(rabi_freq_mhz, axis='x', shape=shape)) return seq
[docs] def rabi_sequence( duration: float, rabi_freq_mhz: float = 1.0, shape: str = "rect" ) -> PulseSequence: """ Create a Rabi oscillation sequence (single pulse of variable duration). Parameters ---------- duration : float Pulse duration in seconds rabi_freq_mhz : float Rabi frequency in MHz shape : str Pulse shape Returns ------- PulseSequence Single-pulse Rabi sequence """ seq = PulseSequence() pulse = Pulse( shape=shape, amplitude=rabi_freq_mhz, duration=duration, phase=0.0 ) seq.add_pulse(pulse) return seq