Source code for sequencing.system

# This file is part of sequencing.
#
#    Copyright (c) 2021, The Sequencing Authors.
#    All rights reserved.
#
#    This source code is licensed under the BSD-style license found in the
#    LICENSE file in the root directory of this source tree.

import re
import json
from functools import reduce
from collections import defaultdict
from contextlib import contextmanager

import numpy as np
import qutip
import attr

from .parameters import Parameterized, ListParameter, DictParameter
from .modes import Mode, sort_modes


[docs]class CouplingTerm(object): """An object representing a coupling between multiple ``Modes``, given by a Hamiltonian term of the form ``strength * product(operators)``. If the keyword argument ``add_hc`` is provided and is True, then the Hamiltonian term takes the form ``strength * (product(operators) + product(operators).dag())``. Args: terms (list[tuple[Mode, str]]): List of tuples of ``(mode, expr)``, which defines the coupling. Each ``expr`` is a string containing an algebraic expression involving the Mode's operators. See :func:`Mode.operator_expr` for more details. The resulting operators are given by ``mode.operator_expr(expr)``. strength (optional, float): Coefficient parameterizing the strength of the coupling. Strength should be given in units of 2 * pi * GHz. Default: 1. add_hc (optional, bool): Whether to add the Hermitian conjugate of the product of the operators. Default: False. """ def __init__(self, *terms, strength=1, add_hc=False): if len(terms) == 1: # terms = ([(mode_1, expr_1), ..., (mode_n, expr_n)], ) # so we want terms[0] if not isinstance(terms[0], (list, tuple)): raise TypeError( f"Expected a list of (Mode, str), but got {type(terms)}." ) terms = terms[0] elif len(terms) == 4 and all(isinstance(item, (Mode, str)) for item in terms): # This is the old two-mode only syntax mode1, op1_expr, mode2, op2_expr = terms terms = [(mode1, op1_expr), (mode2, op2_expr)] for mode, expr in terms: if not isinstance(mode, Mode): raise TypeError(f"Expected instance of Mode, but got {type(mode)}.") if not isinstance(expr, str): raise TypeError(f"Expected instance of str, but got {type(expr)}.") self.terms = list(terms) self.strength = float(strength) self.add_hc = bool(add_hc) @property def operators(self): return [mode.operator_expr(expr) for mode, expr in self.terms]
[docs] def H(self, strength=None, add_hc=None): """Returns the operator representing the coupling term. Args: strength (optional, float): Coefficient parameterizing the strength of the coupling. Strength should be given in units of 2 * pi * GHz. Defaults to self.strength. add_hc (optional, bool): Whether to add the Hermitian conjugate of product of self.operators. Defaults to self.add_hc. Returns: ``qutip.Qobj``: Operator representing the coupling term. """ if strength is None: strength = self.strength if add_hc is None: add_hc = self.add_hc op = reduce(lambda a, b: a * b, self.operators) if add_hc: op = op + op.dag() return strength * op
def __repr__(self): strings = [ f"{type(self).__name__}([", ", ".join([f"({mode.name}, '{expr}')" for mode, expr in self.terms]), f"], strength={self.strength:.3e}", f", add_hc={self.add_hc})", ] return "".join(strings)
[docs]@attr.s class System(Parameterized): """A collection of ``Modes`` that can be coupled together. Attributes: modes (list[Mode]): List of all Modes in the system. coupling_terms (dict[frozenset[str], list[CouplingTerm]]): A dictionary of CouplingTerm objects specifying all interactions in the system. cross_kerrs (dict[frozenset[str], float]): A dictionary of cross-Kerr values in units of GHz. """ modes = ListParameter() cross_kerrs = DictParameter() order_modes = True
[docs] def initialize(self): super().initialize() if self.order_modes: self.modes = sort_modes(self.modes) self._dt = 1 self.active_modes = self.modes self.coupling_terms = defaultdict(list)
@property def dt(self): return self._dt @dt.setter def dt(self, dt): self._dt = dt for mode in self.modes: mode.dt = dt def __getattribute__(self, name): # Access modes like system.qubit. # System.modes can be changed at any time, so we cannot # just setattr(self, name, mode) in initialize(). try: for mode in object.__getattribute__(self, "modes"): if mode.name == name: return mode except AttributeError: pass return object.__getattribute__(self, name)
[docs] def get_mode(self, mode): """Fetch a mode by name. Args: mode (str or Mode): Name of Mode to fetch, or the Mode itself. Returns: Mode: The requested ``Mode``. """ if isinstance(mode, Mode): mode = mode.name if mode not in [m.name for m in self.modes]: raise ValueError(f"{mode} is not a mode of {self.name}.") return getattr(self, mode)
@property def levels(self): """Dictionary of (mode_name, number_of_levels)""" return {mode.name: mode.levels for mode in self.modes} @property def active_modes(self): """List of the modes currently being used.""" for mode in self._active_modes: if mode not in self.modes: raise ValueError( f"{mode.name} is not in {self.name}.modes. " f"This likely happened because {self.name}.modes " f"was changed after {self.name} was created." f"Please set {self.name}.active_modes to be " f"a subset of {self.name}.modes." ) return self._active_modes @active_modes.setter def active_modes(self, modes): if isinstance(modes[0], str): modes = [getattr(self, m) for m in modes] if self.order_modes: modes = sort_modes(modes) if not all(mode in self.modes for mode in modes): raise ValueError("active_modes must be a subset of the system's modes.") self._active_modes = modes for mode in self._active_modes: mode.space = self._active_modes
[docs] @contextmanager def use_modes(self, modes): """A context manager that temporarily sets ``self.active_modes`` to ``modes``, then reverts ``self.active_modes`` to its previous value. Args: modes (list[Mode]): List of ``Modes`` to temporarily assign to ``self.active_modes``. """ if isinstance(modes, (str, Mode)): modes = [modes] old_modes = self.active_modes try: self.active_modes = modes yield finally: self.active_modes = old_modes
[docs] @staticmethod def tensor(*args): """Calculates the tensor product of input operators.""" return qutip.tensor(*args)
[docs] def I(self, modes=None): # noqa: E741, E743 """Identity operator. Args: modes (optional, list[Mode]): List of Modes to use in constructing H0. If None, will use self.active_modes. Default: None. Returns: ``qutip.Qobj``: Identity operator on the Hilbert space defined by ``modes``. """ if modes is None: modes = self.active_modes elif not all(mode in self.modes for mode in modes): raise ValueError("modes must be a subset of the system's modes.") return self.tensor(*[qutip.qeye(mode.levels) for mode in modes])
eye = I
[docs] def fock(self, *args, **kwargs): """Returns a product state in the Fock basis. States can be specified either positionally or as keyword arguments. Args: *args (tuple): Fock states of Modes in the order of self.modes. **kwargs (dict): Fock states of Modes specified as keyword arguments, mode_name=n. Returns: ``qutip.Qobj``: The requested product state. """ if args: if kwargs: raise ValueError( "If positional arguments are provided, " "no keyword arguments are allowed." ) if len(args) != len(self.active_modes): raise ValueError( "The number of positional argument must match " "the number of active modes." ) states = [ qutip.fock(mode.levels, val) for mode, val in zip(self.active_modes, args) ] else: states = [ qutip.fock(mode.levels, kwargs.get(mode.name, 0)) for mode in self.active_modes ] return self.tensor(*states)
[docs] def fock_dm(self, *args, **kwargs): """Returns a product state in the Fock basis, as a density matrix. States can be specified either positionally or as keyword arguments. Args: *args (tuple): Fock states of Modes in the order of self.modes. **kwargs (dict): Fock states of Modes specified as keyword arguments, mode_name=n. Returns: ``qutip.Qobj``: The requested product state, as a density matrix. """ ket = self.fock(*args, **kwargs) return qutip.ket2dm(ket)
basis = fock
[docs] def ground_state(self): """Returns the ground state of the system. Returns: ``qutip.Qobj``: The system's ground state. """ return self.fock()
[docs] def logical_basis(self, *args, **kwargs): """Returns a product state in the basis spanned by the logical states of all modes. Logical states can be specified either positionally or as keyword arguments. Args: *args (tuple): Logical states of Modes in the order of self.modes. **kwargs (dict): Logical states of Modes specified as keyword arguments, mode_name=n. Returns: ``qutip.Qobj``: The requested product state. """ if args: if kwargs: raise ValueError( "If positional arguments are provided, " "no keyword arguments are allowed." ) if len(args) != len(self.active_modes): raise ValueError( "The number of positional argument must match " "the number of active modes." ) states = [ mode.logical_states(full_space=False)[val] for mode, val in zip(self.active_modes, args) ] else: states = [ mode.logical_states(full_space=False)[kwargs.get(mode.name, 0)] for mode in self.active_modes ] return self.tensor(*states)
[docs] def set_cross_kerr(self, mode1, mode2, chi=0): """Set the cross-Kerr (in GHz) between two modes. Note that the order of mode1 and mode2 doesn't matter. Args: mode1 (Mode or str): Instance of Mode or the name of a member of ``self.modes``. mode2 (Mode or str): Instance of Mode or the name of a member of ``self.modes``. chi (optional, float): Cross-Kerr between mode0 and mode1 in GHz. Default: 0. """ if isinstance(mode1, str): mode1 = self.get_mode(mode1) if isinstance(mode2, str): mode2 = self.get_mode(mode2) if mode1 is mode2: raise ValueError("If mode1 is mode2, then it's not a cross-Kerr.") key = frozenset([mode1.name, mode2.name]) # Replace this cross-Kerr if it already exists if key in self.coupling_terms: for i, coupling_term in enumerate(self.coupling_terms[key][:]): subterms = coupling_term.terms if ( subterms[0][0] is mode1 and subterms[0][1] == "n" and subterms[1][0] is mode2 and subterms[1][1] == "n" ) or ( subterms[0][0] is mode2 and subterms[0][1] == "n" and subterms[1][0] is mode1 and subterms[1][1] == "n" ): _ = self.coupling_terms[key].pop(i) self.coupling_terms[key].append( CouplingTerm([(mode1, "n"), (mode2, "n")], strength=2 * np.pi * chi) ) self.cross_kerrs[key] = chi
[docs] def couplings(self, modes=None, clean=True): """Returns all of the static coupling terms in the Hamiltonian. Args: modes (optional, list[Mode]): List of Modes to use in constructing H0. If None, will use self.active_modes. Default: None. clean (optional, bool): Only keep operators with nonzero elements. Default: True. Returns: list[qutip.Qobj]: List of static coupling terms. """ if modes is None: modes = self.active_modes mode_names = set(mode.name for mode in modes) coupling_terms = [] with self.use_modes(modes): for names, terms in self.coupling_terms.items(): if set(names).issubset(mode_names): coupling_terms.extend([term.H() for term in terms]) if clean: return [term for term in coupling_terms if term.data.nnz] return coupling_terms
[docs] def H0(self, modes=None, clean=True): """Returns the static Hamiltonian consisting of all self-Kerrs and inter-mode couplings. Args: modes (optional, list[Mode]): List of Modes to use in constructing H0. If None, will use self.active_modes. Default: None. clean (optional, bool): Only keep operators with nonzero elements. Default: True. Returns: list[qutip.Qobj]: Static Hamiltonian in list form. """ if modes is None: modes = self.active_modes detunings = [mode.detuning for mode in modes] self_kerrs = [mode.self_kerr for mode in modes] couplings = self.couplings(modes=modes, clean=clean) H0 = detunings + self_kerrs + couplings if clean: return [H for H in H0 if H.data.nnz] return H0
[docs] def c_ops(self, modes=None, clean=True): """Returns a list of collapse operators corresponding to loss (decay/excitation) and dephasing of all modes. Args: modes (optional, list[Mode]): List of Modes to use in constructing H0. If None, will use self.active_modes. Default: None. clean (optional, bool): Only keep operators with nonzero elements. Default: True. Returns: list[qutip.Qobj]: List of all collapse operators. """ if modes is None: modes = self.active_modes decay = [mode.decay for mode in modes] excitation = [mode.excitation for mode in modes] dephasing = [mode.dephasing for mode in modes] c_ops = decay + excitation + dephasing if clean: return [c for c in c_ops if c.data.nnz] return c_ops
[docs] def as_dict(self, json_friendly=False): """Overrides Parameterized.as_dict() in order to deal with cross_kerrs. Args: json_friendly (optional, bool): Whether to return a JSON-friendly dictionary. Default:True. Returns: dict: Dictionary representation of the System object. """ d = super().as_dict(json_friendly=json_friendly) cross_kerrs = d.pop("cross_kerrs") d["cross_kerrs"] = {} if json_friendly: # turn frozenset({mode0, mode1}) into '{mode0, mode1}' for json for key, val in cross_kerrs.items(): new_key = "{" + ", ".join(key) + "}" d["cross_kerrs"][new_key] = val else: for key, val in cross_kerrs.items(): d["cross_kerrs"][key] = val return d
[docs] @classmethod def from_json(cls, json_path=None, json_str=None): """Overrides Parameterized.from_json() in order to deal with cross_kerrs. Args: json_path (optional, str): Path to JSON file from which to load parameters. Required if ``json_str`` is ``None``. Default: None. json_str (optional, str): JSON string like that returned by ``self.to_json(dumps=True)``. Required if ``json_path`` is ``None`` Default: None. Returns: System: Instance of ``System`` whose parameters have been populated from the JSON data. """ def json_decode(obj): # decode CrossKerr for key in list(obj): if re.match(r"\{(.*?)\}", key): # turn '{mode0, mode1}' into frozenset({mode0, mode1}) k = key.replace("{", "").replace("}", "").split(", ") new_key = frozenset(k) obj[new_key] = obj.pop(key) return obj if json_str is not None: if json_path is not None: raise ValueError( "You must provide either json_path " "or json_str, not both." ) d = json.loads(json_str, object_hook=json_decode) else: if json_path is None: raise ValueError("You must provide either json_path or json_str.") if not json_path.endswith(".json"): json_path = json_path + ".json" with open(json_path, "r") as f: d = json.load(f, object_hook=json_decode) return cls.from_dict(d)