Source code for qibolab._core.instruments.emulator.hamiltonians

from functools import cached_property
from typing import Literal, Optional, Union

import numpy as np
from pydantic import Field
from qibo.config import raise_error
from scipy.constants import giga

from ...components import Config
from ...identifier import QubitId, QubitPairId, TransitionId
from ...parameters import Update, _setvalue
from ...pulses import Delay, Pulse, PulseLike, VirtualZ
from ...serialize import Model
from .engine import Operator, SimulationEngine

__all__ = ["DriveEmulatorConfig", "FluxEmulatorConfig", "HamiltonianConfig"]


[docs] class DriveEmulatorConfig(Config): """Configuration for an IQ channel.""" kind: Literal["drive-emulator"] = "drive-emulator" frequency: float """Frequency of drive.""" rabi_frequency: float = 1e9 """Rabi frequency [Hz]""" scale_factor: float = 1 """Scaling factor."""
[docs] @staticmethod def operator(n: int, engine: SimulationEngine) -> Operator: return -1j * (engine.destroy(n) - engine.create(n))
[docs] class FluxEmulatorConfig(Config): """Configuration for a flux line.""" kind: Literal["flux-emulator"] = "flux-emulator" offset: float """DC offset of the channel.""" voltage_to_flux: float = 1 """Convert voltarget to flux."""
[docs] @staticmethod def operator(n: int, engine: SimulationEngine) -> Operator: return engine.create(n) * engine.destroy(n)
@property def flux(self) -> float: """Returns flux.""" return self.offset * self.voltage_to_flux
class Qubit(Config): """Hamiltonian parameters for single qubit.""" frequency: float = 0 """Qubit frequency for 0->1.""" anharmonicity: float = 0 """Qubit anharmonicity.""" sweetspot: float = 0 """Sweetspot point.""" asymmetry: float = 0 """Asymmetry.""" t1: dict[TransitionId, float] = Field(default_factory=dict) """Dictionary with relaxation times per transition.""" t2: dict[TransitionId, float] = Field(default_factory=dict) """Dictionary with dephasing time per transition.""" def omega(self, flux: float = 0) -> float: """Angular velocity.""" return 2 * np.pi * self.detuned_frequency(flux) def detuned_frequency(self, flux: float) -> float: """Return frequency of the qubit modified by the flux.""" return (self.frequency - self.anharmonicity) * ( self.asymmetry**2 + (1 - self.asymmetry**2) * np.cos(np.pi * (flux - self.sweetspot)) ** 2 ) ** (1 / 4) + self.anharmonicity def operator(self, n: int, engine: SimulationEngine, flux: float = 0): """Time independent operator.""" quadratic_term = engine.create(n) * engine.destroy(n) * self.omega(flux) / giga quartic_term = ( self.anharmonicity * np.pi / giga * engine.create(n) * engine.create(n) * engine.destroy(n) * engine.destroy(n) ) return quadratic_term + quartic_term def t_phi(self, transition: TransitionId) -> float: """T_phi computed from T1 and T2 per transition.""" return 1 / (1 / self.t2[transition] - 1 / self.t1[transition] / 2) def relaxation(self, n: int, engine: SimulationEngine) -> Operator: return sum( np.sqrt(1 / t1) * engine.basis(state=transition[0], dim=n) * engine.basis(state=transition[1], dim=n).dag() for transition, t1 in self.t1.items() ) def dephasing(self, n: int, engine: SimulationEngine) -> Operator: return sum( np.sqrt(1 / self.t_phi(pair) / 2) * ( engine.basis(state=pair[0], dim=n) * engine.basis(state=pair[0], dim=n).dag() - engine.basis(state=pair[1], dim=n) * engine.basis(state=pair[1], dim=n).dag() ) for pair in self.t2 ) class CapacitiveCoupling(Config): """Hamiltonian parameters for qubit pair.""" coupling: float """Qubit-qubit coupling.""" @staticmethod def _operator(n: int, engine: SimulationEngine) -> Operator: """Time independent operator.""" op = engine.tensor( [ engine.destroy(n), engine.create(n), ] ) + engine.tensor( [ engine.create(n), engine.destroy(n), ] ) return 2 * np.pi * op / giga def operator(self, n: int, engine: SimulationEngine) -> Operator: """Time independent operator.""" return self.coupling * self._operator(n, engine) class FluxPulse(Model): """Flux pulse term in Hamiltonian.""" pulse: Pulse """Flux pulse to be played.""" config: FluxEmulatorConfig """Flux emulator configuration.""" qubit: Qubit """Qubit affected by the flux pulse.""" sampling_rate: float = 1 """Sampling rate.""" @cached_property def envelopes(self): """Pulse envelopes.""" return self.pulse.envelopes(self.sampling_rate) @property def duration(self): """Duration of the pulse.""" return self.pulse.duration @property def phase(self): """Virtual Z phase.""" return 0 def __call__(self, t, sample, phase): i, _ = self.envelopes # we are passing the relative frequency because the term with the offset # is already included in the time-independent part of the Hamiltonian # and it corresponds to changing the static bias return ( 2 * np.pi * ( self.qubit.detuned_frequency( self.config.voltage_to_flux * (i[sample] + self.config.offset) ) - self.qubit.detuned_frequency(self.config.flux) ) / giga ) class ModulatedDrive(Model): """Hamiltonian parameters for qubit drive.""" pulse: Pulse """Drive pulse.""" config: DriveEmulatorConfig """Drive emulator configuration.""" phase: float = 0 """Drive has zero virtual z phase.""" sampling_rate: float = 1 """Sampling rate.""" @cached_property def envelopes(self): """Pulse envelopes.""" return self.pulse.envelopes(self.sampling_rate) @property def duration(self): """Duration of the pulse.""" return self.pulse.duration @property def omega(self): return 2 * np.pi * self.config.frequency / giga @property def rabi_omega(self): return 2 * np.pi * self.config.rabi_frequency / giga def __call__(self, t, sample, phase): i, q = self.envelopes phi = self.omega * t + self.pulse.relative_phase + phase return ( self.rabi_omega * self.config.scale_factor * (np.cos(phi) * i[sample] + np.sin(phi) * q[sample]) ) class ModulatedDelay(Model): """Modulated delay.""" duration: float """Delay duration.""" phase: float = 0 """Delay has 0 virtual z phase.""" def __call__(self, t: float, sample: int, phase: float) -> float: """Delay waveform.""" return 0 class ModulatedVirtualZ(Model): """Modulated Virtual Z pulse.""" phase: float """Virtual Z phase.""" duration: float = 0 """Duration is 0 for virtual Z.""" def __call__(self, t: float, sample: int, phase: float) -> float: """Delay waveform.""" raise_error(ValueError, "VirtualZ doesn't have waveform.") Modulated = Union[ModulatedDrive, ModulatedDelay, ModulatedVirtualZ] ControlLine = Union[Modulated, FluxPulse]
[docs] class HamiltonianConfig(Config): """Hamiltonian configuration.""" kind: Literal["hamiltonian"] = "hamiltonian" transmon_levels: int = 2 qubits: dict[QubitId, Qubit] = Field(default_factory=dict) pairs: dict[QubitPairId, CapacitiveCoupling] = Field(default_factory=dict) @property def nqubits(self): """Number of qubits.""" return len(self.qubits)
[docs] def replace(self, update: Update) -> "HamiltonianConfig": """Update parameters' values.""" d = self.model_dump() for path, val in update.items(): _setvalue(d, path, val) return self.model_validate(d)
[docs] def initial_state(self, engine: SimulationEngine) -> Operator: """Initial state as ground state of the system.""" return engine.basis(self.dims, self.nqubits * [0])
[docs] def hilbert_space_index(self, qubit: QubitId) -> int: """Return Hilbert space index from qubit id.""" return list(self.qubits).index(qubit)
@property def dims(self) -> list[int]: """Dimensions of the system.""" return [self.transmon_levels] * len(self.qubits)
[docs] def hamiltonian(self, config: dict, engine: SimulationEngine) -> Operator: """Time independent part of Hamiltonian.""" qubit_terms = sum( engine.expand( qubit.operator( n=self.transmon_levels, flux=static_flux(qubit=i, config=config), engine=engine, ), self.dims, self.hilbert_space_index(i), ) for i, qubit in self.qubits.items() ) coupling = sum( engine.expand( pair.operator(self.transmon_levels, engine), self.dims, [ self.hilbert_space_index(pair_id[0]), self.hilbert_space_index(pair_id[1]), ], ) for (pair_id, pair) in self.pairs.items() ) return qubit_terms + coupling
[docs] def dissipation(self, engine: SimulationEngine) -> Operator: """Dissipation operators for the hamiltonian. They are going to be passed to mesolve as collapse operators.""" collapse_operators = [] for i, qubit in self.qubits.items(): if len(qubit.t1) > 0: collapse_operators.append( engine.expand( qubit.relaxation(self.transmon_levels, engine), self.dims, self.hilbert_space_index(i), ) ) if len(qubit.t2) > 0: collapse_operators.append( engine.expand( qubit.dephasing(self.transmon_levels, engine), self.dims, self.hilbert_space_index(i), ) ) return collapse_operators
def static_flux(qubit: QubitId, config: dict) -> float: """Get static flux for qubit given config (offset).""" qubit_config = config.get(f"{qubit}/flux") if qubit_config is not None: return qubit_config.flux coupler_config = config.get(f"coupler_{qubit}/flux") if coupler_config is not None: return coupler_config.flux return 0 def waveform( pulse: PulseLike, config: Config, qubit: Qubit, ) -> Optional[ControlLine]: """Convert pulse to hamiltonian.""" if not isinstance(config, (DriveEmulatorConfig, FluxEmulatorConfig)): return None if isinstance(pulse, Pulse): if isinstance(config, DriveEmulatorConfig): return ModulatedDrive(pulse=pulse, config=config) if isinstance(config, FluxEmulatorConfig): return FluxPulse( pulse=pulse, config=config, qubit=qubit, ) if isinstance(pulse, Delay): return ModulatedDelay(duration=pulse.duration) if isinstance(pulse, VirtualZ): return ModulatedVirtualZ(phase=pulse.phase)