Source code for scqubits.core.circuit_noise

# circuit_noise.py
#
# This file is part of scqubits: a Python package for superconducting qubits,
# Quantum 5, 583 (2021). https://quantum-journal.org/papers/q-2021-11-17-583/
#
#    Copyright (c) 2019 and later, Jens Koch and Peter Groszkowski
#    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.
############################################################################

from abc import ABC
from numpy import ndarray
import numpy as np
import qutip as qt
import re
import copy

from scqubits.core.noise import NOISE_PARAMS, NoisySystem
from scqubits.core.circuit_utils import get_trailing_number
from scqubits.utils.misc import is_string_float, Qobj_to_scipy_csc_matrix

from types import MethodType
from typing import Callable, Optional, Tuple, Union

import sympy as sm

from scqubits.core.symbolic_circuit import Branch


[docs] class NoisyCircuit(NoisySystem, ABC): """This class represents a noisy quantum circuit, extending the :class:`NoisySystem` class. It provides a suite of methods for generating and evaluating expressions related to the Hamiltonian and its derivatives, as well as calculating the 1/f dephasing time or rate due to various noise sources. Attributes ---------- noise_helper_methods: A dictionary that stores the noise helper methods generated by :meth:`generate_methods_d_hamiltonian_d`. Notes ----- This class is designed to be subclassed and extended with specific noise models for different types of quantum circuits. """
[docs] @staticmethod def Q_from_branch(branch: Branch): """Extracts the Quality Factor 'Q' from a Given Branch. This method performs the following steps: 1. Determines the type of the branch ('L' for inductor or 'C' for capacitor) and constructs a key for the quality factor ('Q_ind' for inductor or 'Q_cap' for capacitor). 2. Checks if the key is in the auxiliary parameters of the branch. If not, the method returns None. 3. If the key is in the auxiliary parameters, it retrieves the value of the quality factor, which is a string. 4. Checks if the string can be evaluated as a float using the 'is_string_float' helper function. If it can, it converts the string to a float and returns it. 5. If the string cannot be evaluated as a float, it assumes that the string represents a function of frequency 'omega' and temperature 'T'. It defines a new function 'Q_func' that takes 'omega' and 'T' as parameters and evaluates the string using Python's 'eval' function. It returns 'Q_func'. Parameters ---------- branch: The branch from which to extract the quality factor. The branch should be an instance of the 'Branch' class. It should have a 'type' attribute that is either 'L' or 'C', and an 'aux_params' dictionary that contains a key 'Q_ind' or 'Q_cap' with the quality factor as the value. Returns -------- float or function: The quality factor as a float, or a function that calculates the quality factor given frequency and temperature. If the branch does not have a quality factor, None is returned. Raises ------ AttributeError: If the branch does not have a 'type' attribute or an 'aux_params' dictionary. ValueError: If the branch's type is not 'L' or 'C', or if the quality factor string cannot be evaluated as a float or a function of 'omega' and 'T'. """ key = "Q_" + ("ind" if branch.type == "L" else "cap") if key in branch.aux_params.keys(): Q_str = branch.aux_params[key] if not is_string_float(Q_str): def Q_func(omega, T): return eval(Q_str) return Q_func else: return float(Q_str) return None
def _d_hamiltonian_d_function_factory(self, param_sym: sm.Symbol) -> Callable: def param_derivative( self, energy_esys: Union[bool, Tuple[ndarray, ndarray]] = False ): parent_instance = self.return_parent_circuit() hamiltonian = parent_instance._hamiltonian_sym_for_numerics hamiltonian = hamiltonian.subs("I", 1) all_sym_parameters = ( list(parent_instance.symbolic_params.keys()) + parent_instance.external_fluxes + parent_instance.offset_charges ) diff_sym_expr = hamiltonian.diff(param_sym) diff_sym_expr = diff_sym_expr.expand() # substitute all symbolic params for param in all_sym_parameters: diff_sym_expr = diff_sym_expr.subs( param, getattr(parent_instance, param.name) ) # evaluate the expression native = parent_instance._evaluate_symbolic_expr(diff_sym_expr) return self.process_op(native_op=native, energy_esys=energy_esys) return param_derivative def _generate_methods_d_hamiltonian_d(self): """This method generates methods that return the derivative of the Hamiltonian with respect to offset charges, external fluxes, and junction energies. Steps: ------ 1. Initializes three dictionaries to store the generated methods: :meth:`ext_flux_1_over_f_methods` for methods related to external fluxes, :meth:`ng_1_over_f_methods` for methods related to offset charges, and :meth:`cc_1_over_f_methods` for methods related to junction energies. 2. Iterates over the external fluxes (:attr:`external_fluxes`) and offset charges (:attr:`offset_charges`) of the current instance. For each parameter, it defines a new method `param_derivative` that calculates the derivative of the Hamiltonian with respect to that parameter. This is done by first fetching the symbolic Hamiltonian of the parent circuit, differentiating it with respect to the parameter using sympy's `diff` function, substituting all symbolic parameters with their actual values, and finally evaluating the expression. The new method is then stored in the corresponding dictionary with a key that is based on the parameter's name. 3. Iterates over the branches (:attr:`branches`) of the current instance. For each branch that is a junction (i.e., its type contains 'JJ'), it defines a new method `param_derivative` that calculates the derivative of the Hamiltonian with respect to the junction energy. This is done by calling the `_junction_related_evaluation` method with the branch and the calculation type 'dhdEJ'. The new method is then stored in the corresponding dictionary with a key that is based on the branch's ID. 4. Combines the dictionaries of methods into a single dictionary (:attr:`noise_helper_methods`) and stores this dictionary in the 'noise_helper_methods' attribute of the current instance. 5. Adds each method to the current instance as an attribute with the same name as the key in the dictionary using `setattr`. The generated methods use the 'return_parent_circuit' method to get a parent circuit with a symbolic Hamiltonian and symbolic parameters, the '_evaluate_symbolic_expr' method to evaluate a symbolic expression, and the '_junction_related_evaluation' method to calculate a quantity related to a junction. Raises ------ AttributeError: If the current instance does not have the required methods or attributes. Note ---- This method modifies the current instance by adding new attributes and updating the 'noise_helper_methods' attribute. """ ext_flux_1_over_f_methods = {} ng_1_over_f_methods = {} cc_1_over_f_methods = {} for param_sym in self.external_fluxes + self.offset_charges: param_derivative = self._d_hamiltonian_d_function_factory(param_sym) if param_sym in self.external_fluxes: ext_flux_1_over_f_methods[ f"d_hamiltonian_d_flux{get_trailing_number(param_sym.name)}" ] = param_derivative elif param_sym in self.offset_charges: ng_1_over_f_methods[ f"d_hamiltonian_d_ng{get_trailing_number(param_sym.name)}" ] = param_derivative ## cc noise methods junction_branches = [branch for branch in self.branches if "JJ" in branch.type] for idx, branch in enumerate(junction_branches): def param_derivative(self, branch=branch): return -self._junction_related_evaluation(branch, calc="dhdEJ") cc_1_over_f_methods[f"d_hamiltonian_d_EJ{branch.index}"] = param_derivative noise_helper_methods = { **ext_flux_1_over_f_methods, **ng_1_over_f_methods, **cc_1_over_f_methods, } self.noise_helper_methods = noise_helper_methods for method_name in noise_helper_methods: setattr( self, method_name, MethodType(noise_helper_methods[method_name], self) ) def _transform_expr_to_new_variables( self, expr_node_vars: sm.Expr, substitute_symbol: Optional[str] = None ): """This method transforms a symbolic expression that represents a physical quantity in the circuit from the old variables to the new variables. Steps: 1. Retrieves the transformation matrix from the current instance. 2. Expands the input expression, which is a symbolic expression that represents a physical quantity in the circuit (e.g., the Hamiltonian, a current, a voltage, etc.) in terms of the old variables. 3. Determines the number of variables based on the number of nodes in the symbolic circuit of the current instance. If the circuit is grounded, one is subtracted from the number of nodes. 4. Generates a list of new variables and a list of old variables. The new variables are named 'θ' followed by an index, and the old variables are named 'φ' followed by an index. 5. Calculates the transformed expression by multiplying the transformation matrix with the new variables. 6. Substitutes each old variable in the input expression with the corresponding term from the transformed expression. 7. If a substitute symbol is provided, substitutes each free symbol in the input expression with a new symbol that has the substitute symbol followed by the trailing number of the old symbol. Parameters ---------- expr_node_vars (sm.Expr): The symbolic expression to transform. This should be a sympy expression that represents a physical quantity in the circuit (e.g., the Hamiltonian, a current, a voltage, etc.) in terms of the old variables. substitute_symbol (str, optional): The symbol to use for the new variables. If this is provided, each free symbol in the input expression is replaced with a new symbol that has this substitute symbol followed by the trailing number of the old symbol. Defaults to None. Returns ------- sm.Expr: The transformed expression. This is a sympy expression that represents the same physical quantity as the input expression, but in terms of the new variables. Raises ------ AttributeError: If the current instance does not have the required methods or attributes. """ transformation_mat = self.transformation_matrix expr_node_vars = expr_node_vars.expand() num_vars = len(self.symbolic_circuit.nodes) - (1 if self.is_grounded else 0) new_vars = [sm.symbols(f{index}") for index in range(1, 1 + num_vars)] old_vars = [sm.symbols(f{index}") for index in range(1, 1 + num_vars)] transformed_expr = transformation_mat.dot(new_vars) for idx, var in enumerate(old_vars): expr_node_vars = expr_node_vars.subs(var, transformed_expr[idx]) if substitute_symbol: for var in expr_node_vars.free_symbols: expr_node_vars = expr_node_vars.subs( var, sm.symbols(f"{substitute_symbol}{get_trailing_number(var.name)}"), ) return expr_node_vars def _junction_related_evaluation(self, branch_junction: Branch, calc="dhdEJ"): """This method evaluates a physical quantity related to a specific junction in the quantum circuit. Steps: 1. Retrieves the parent circuit and its symbolic Hamiltonian. The parent circuit is the circuit that contains the junction, and the symbolic Hamiltonian is a sympy expression that represents the Hamiltonian of the parent circuit in terms of symbolic variables. 2. Substitutes the offset charges and symbolic parameters of the parent circuit into the Hamiltonian. The offset charges and symbolic parameters are attributes of the parent circuit that represent physical quantities in the circuit. 3. Substitutes the imaginary unit 'I' with 1 in the Hamiltonian. This is done to simplify the Hamiltonian for the subsequent calculations. 4. Calculates an expression that represents the cosine of the phase difference across the junction. The phase difference is represented by the difference of the phases at the two nodes of the junction. 5. Transforms the cosine expression to new variables using the `_transform_expr_to_new_variables` method of the parent circuit. This method transforms the expression from the old variables (the phases at the nodes) to the new variables (the variables in the transformed Hamiltonian). 6. Finds the term in the Hamiltonian that matches the transformed cosine expression, ignoring any external fluxes. This is done by iterating over the terms in the Hamiltonian and comparing each term (with the external fluxes set to zero) to the transformed cosine expression. 7. Substitutes the external fluxes into the found term. The external fluxes are attributes of the parent circuit that represent physical quantities in the circuit. 8. If the 'calc' parameter is 'sin_phi_qp', replaces the cosine in the term with a sine and divides the argument of the sine by 2. This is done to calculate a different quantity that is related to the junction. 9. Evaluates the final term using the `_evaluate_symbolic_expr` method of the parent circuit. This method evaluates a sympy expression by substituting the actual values of the variables into the expression. Parameters ---------- branch_junction (Branch): The junction for which to calculate the quantity. This should be a branch of the current instance's circuit that represents a junction. calc (str, optional): The type of calculation to perform. If this is 'sin_phi_qp', the cosine in the term is replaced with a sine and the argument of the sine is divided by 2. This is used to calculate a different quantity that is related to the junction. Defaults to 'dhdEJ'. Returns ------- float: The evaluated quantity. This is a numerical value that represents the calculated quantity related to the junction. Raises ------ AttributeError: If the current instance or the parent circuit does not have the required methods or attributes. """ parent_instance = self.return_parent_circuit() hamiltonian = parent_instance._hamiltonian_sym_for_numerics for sym in parent_instance.offset_charges + list( parent_instance.symbolic_params.keys() ): hamiltonian = hamiltonian.subs(sym, getattr(parent_instance, sym.name)) hamiltonian = hamiltonian.subs("I", 1) branch_cos_node_expr = sm.cos( sm.symbols(f{branch_junction.nodes[0].index}") - sm.symbols(f{branch_junction.nodes[1].index}") ) branch_cos_node_expr = branch_cos_node_expr.subs( "φ0", 0 ) # setting ground node to zero. branch_cos_expr = parent_instance._transform_expr_to_new_variables( branch_cos_node_expr ) expr_dict = hamiltonian.as_coefficients_dict() for term, coefficient in expr_dict.items(): term_without_ext_flux = copy.copy(term) for flux in parent_instance.external_fluxes: term_without_ext_flux = term_without_ext_flux.subs(flux, 0) if term_without_ext_flux == branch_cos_expr: break # substitute external flux for flux in parent_instance.external_fluxes: term = term.subs(flux, getattr(parent_instance, flux.name)) if calc == "sin_phi_qp": term = term.subs(sm.cos, sm.sin) term = term.subs(term.args[0], term.args[0] / 2) # evaluate the expression return parent_instance._evaluate_symbolic_expr(term) def _tphi_1_over_f_function_factory(self, noise_type: str, noise_op_func: Callable): def tphi_1_over_f_func( self=self, A_noise: float = NOISE_PARAMS[f"A_{noise_type}"], i: int = 0, j: int = 1, esys: Tuple[ndarray, ndarray] = None, get_rate: bool = False, **kwargs, ) -> float: """This function calculates the 1/f dephasing time (or rate) due to a specific type of noise in the quantum circuit. Refer to the name of the method to determine the type of noise. Parameters ---------- A_noise: The strength of the noise. This is a scaling factor that determines the magnitude of the noise. i: The state index that, along with `j`, defines a qubit. This is an integer that identifies one of the states of the qubit. j: The state index that, along with `i`, defines a qubit. This is an integer that identifies one of the states of the qubit. esys: A tuple containing the eigenvalues and eigenvectors of the system Hamiltonian. This is used to calculate the transition frequencies between the states of the qubit. get_rate: A flag that determines whether to calculate the dephasing time or the dephasing rate. If this is True, the function calculates the dephasing rate; otherwise, it calculates the dephasing time. Returns ------- float: The calculated 1/f dephasing time (or rate). This is a numerical value that represents the time it takes for the qubit to lose phase coherence due to the noise, or the rate at which the qubit loses phase coherence due to the noise. """ noise_op = noise_op_func() if isinstance(noise_op, qt.Qobj): noise_op = Qobj_to_scipy_csc_matrix(noise_op) if isinstance(noise_op, list): noise_op_new = [] for op in noise_op: if isinstance(op, qt.Qobj): op = Qobj_to_scipy_csc_matrix(op) noise_op_new.append(op) noise_op = noise_op_new return self.tphi_1_over_f( A_noise=A_noise, i=i, j=j, noise_op=noise_op, esys=esys, get_rate=get_rate, **kwargs, ) return tphi_1_over_f_func def _generate_tphi_1_over_f_methods(self): """This function is a dynamic method generator, crafting methods for calculating the 1/f dephasing time (or rate) due to different types of noise in the quantum circuit. The generated methods are named `tphi_1_over_f_{noise_type}{index}`, where `noise_type` can be 'cc', 'ng', or 'flux', and `index` differentiates individual noise sources. external_fluxes : A collection of symbols representing external fluxes in the circuit. offset_charges : A collection of symbols representing offset charges in the circuit. branches : A collection of branches in the circuit. Notes ----- 1. Identifies the branches in the circuit that are junctions (indicated by "JJ" in the branch type). 2. Initializes dictionaries to store the generated methods for each type of noise. 3. Iterates over the external fluxes, offset charges, and junction branches in the circuit. 4. Depending on the type of parameter (external flux, offset charge, or junction branch), it determines the type of noise and the function to differentiate the Hamiltonian with respect to the parameter. 5. If the parameter is an expression, it extracts the trailing number from the parameter name and uses it to get the appropriate differentiation function from the current instance. 6. If the parameter is a junction branch, it uses the branch's ID string as the trailing number and gets the appropriate differentiation function from the current instance. 7. Defines a function `tphi_1_over_f_func` that calculates the 1/f dephasing time (or rate) for the current parameter. This function invokes the differentiation function to generate the noise operator, converts the noise operator to a sparse matrix if necessary, and then calls the `tphi_1_over_f` method of the current instance to calculate the 1/f dephasing time (or rate). 8. Adds the `tphi_1_over_f_func` function to the appropriate dictionary, depending on the type of parameter. 9. Merges the dictionaries into a single dictionary `noise_methods`. 10. Iterates over the `noise_methods` dictionary and adds each method as an attribute of the current instance. This function does not return anything; it modifies the current instance by adding the 1/f dephasing time (or rate) calculation methods as attributes. """ # calculating the rates from each of the flux sources junction_branches = [branch for branch in self.branches if "JJ" in branch.type] methods_noise_rates_from_flux = {} methods_noise_rates_from_ng = {} methods_noise_rates_from_cc = {} for param_sym in self.external_fluxes + self.offset_charges + junction_branches: if param_sym in self.external_fluxes: diff_func_name = "d_hamiltonian_d_flux" noise_type = "flux" elif param_sym in self.offset_charges: diff_func_name = "d_hamiltonian_d_ng" noise_type = "ng" if param_sym in junction_branches: diff_func_name = "d_hamiltonian_d_EJ" noise_type = "cc" if isinstance(param_sym, sm.Expr): trailing_number = get_trailing_number(param_sym.name) noise_op_func = getattr(self, f"{diff_func_name}{trailing_number}") elif param_sym in junction_branches: trailing_number = param_sym.index noise_op_func = getattr(self, f"{diff_func_name}{trailing_number}") tphi_1_over_f_func = self._tphi_1_over_f_function_factory( noise_type=noise_type, noise_op_func=noise_op_func ) if param_sym in self.external_fluxes: methods_noise_rates_from_flux[ f"tphi_1_over_f_flux{trailing_number}" ] = tphi_1_over_f_func elif param_sym in self.offset_charges: methods_noise_rates_from_ng[f"tphi_1_over_f_ng{trailing_number}"] = ( tphi_1_over_f_func ) elif param_sym in junction_branches: methods_noise_rates_from_cc[f"tphi_1_over_f_cc{trailing_number}"] = ( tphi_1_over_f_func ) noise_methods = { **methods_noise_rates_from_flux, **methods_noise_rates_from_ng, **methods_noise_rates_from_cc, } for method_name in noise_methods: setattr(self, method_name, MethodType(noise_methods[method_name], self)) def _generate_overall_tphi_cc(self): """ This function calculates the overall dephasing time (Tphi) due to 1/f critical current (cc) noise in the quantum circuit. Steps ----- 1. Checks if there are any existing methods for calculating Tphi due to 1/f cc noise. This is done by searching the methods of the current instance for method names that match the pattern "tphi_1_over_f_cc\\d+$". If such methods exist, the function returns None and does not generate a new method. 2. Defines a new method `tphi_1_over_f_cc` for calculating the overall Tphi due to 1/f cc noise. This method performs the following steps: a. Initializes an empty list `tphi_times` to store the Tphi times for each junction branch in the circuit. b. Iterates over the junction branches in the circuit. A junction branch is a branch that represents a Josephson junction (JJ). c. Calls the method for calculating Tphi due to 1/f cc noise for the current junction branch. The method is an attribute of the current instance that is named "tphi_1_over_f_cc" followed by the ID string of the branch. d. Appends the calculated Tphi time to the `tphi_times` list. e. Calculates the total rate of dephasing by summing the reciprocals of the Tphi times. f. If the `get_rate` parameter is True, returns the total rate of dephasing. Otherwise, returns the reciprocal of the total rate (the overall Tphi time), or infinity if the total rate is zero. 3. Adds the `tphi_1_over_f_cc` method as an attribute of the current instance. This is done using the `setattr` function and the `MethodType` class to bind the method to the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the `tphi_1_over_f_cc` method as an attribute. """ if not any([re.match(r"tphi_1_over_f_cc\d+$", method) for method in dir(self)]): return None def tphi_1_over_f_cc( self=self, A_noise: float = NOISE_PARAMS["A_cc"], i: int = 0, j: int = 1, esys: Tuple[ndarray, ndarray] = None, get_rate: bool = False, **kwargs, ) -> float: r"""Calculate the 1/f dephasing time (or rate) due to critical current noise of junction associated with Josephson energy in the circuit. Parameters ---------- A_noise: noise strength i: state index that along with j defines a qubit j: state index that along with i defines a qubit esys: evals, evecs tuple get_rate: get rate or time Returns ------- decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ tphi_times = [] for branch in [brnch for brnch in self.branches if "JJ" in brnch.type]: tphi_times.append( getattr(self, f"tphi_1_over_f_cc{branch.index}")( A_noise=A_noise, i=i, j=j, esys=esys, **kwargs, ) ) total_rate = sum([1 / tphi for tphi in tphi_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "tphi_1_over_f_cc", MethodType(tphi_1_over_f_cc, self)) def _generate_overall_tphi_flux(self): """ This function calculates the overall dephasing time (Tphi) due to 1/f flux noise in the quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Steps ----- 1. Checks if there are any existing methods for calculating Tphi due to 1/f flux noise. This is done by searching the methods of the current instance for method names that match the pattern "tphi_1_over_f_flux\\d+$". If such methods exist, the function returns None and does not generate a new method. 2. Defines a new method `tphi_1_over_f_flux` for calculating the overall Tphi due to 1/f flux noise. This method performs the following steps: a. Initializes an empty list `tphi_times` to store the Tphi times for each external flux in the circuit. b. Iterates over the external fluxes in the circuit. c. Calls the method for calculating Tphi due to 1/f flux noise for the current external flux. The method is an attribute of the current instance that is named "tphi_1_over_f_flux" followed by the trailing number in the name of the flux. d. Appends the calculated Tphi time to the `tphi_times` list. e. Calculates the total rate of dephasing by summing the reciprocals of the Tphi times. f. If the `get_rate` parameter is True, returns the total rate of dephasing. Otherwise, returns the reciprocal of the total rate (the overall Tphi time), or infinity if the total rate is zero. 3. Adds the `tphi_1_over_f_flux` method as an attribute of the current instance. This is done using the `setattr` function and the `MethodType` class to bind the method to the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the `tphi_1_over_f_flux` method as an attribute. """ if not any( [re.match(r"tphi_1_over_f_flux\d+$", method) for method in dir(self)] ): return None def tphi_1_over_f_flux( self=self, A_noise: float = NOISE_PARAMS["A_flux"], i: int = 0, j: int = 1, esys: Tuple[ndarray, ndarray] = None, get_rate: bool = False, **kwargs, ) -> float: r"""Calculate the 1/f dephasing time (or rate) due to flux noise. Parameters ---------- A_noise: noise strength i: state index that along with j defines a qubit j: state index that along with i defines a qubit esys: evals, evecs tuple get_rate: get rate or time Returns ------- decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ tphi_times = [] for flux_sym in self.external_fluxes: tphi_times.append( getattr( self, f"tphi_1_over_f_flux{get_trailing_number(flux_sym.name)}" )( A_noise=A_noise, i=i, j=j, esys=esys, **kwargs, ) ) total_rate = sum([1 / tphi for tphi in tphi_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "tphi_1_over_f_flux", MethodType(tphi_1_over_f_flux, self)) def _generate_overall_tphi_ng(self): """ Overall Dephasing Time (Tphi) Calculator due to 1/f Flux Noise -------------------------------------------------------------- This function calculates the overall dephasing time (Tphi) due to 1/f flux noise in the quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Steps ----- 1. Checks if there are any existing methods for calculating Tphi due to 1/f flux noise. This is done by searching the methods of the current instance for method names that match the pattern "tphi_1_over_f_flux\\d+$". If such methods exist, the function returns None and does not generate a new method. 2. Defines a new method `tphi_1_over_f_flux` for calculating the overall Tphi due to 1/f flux noise. This method performs the following steps: a. Initializes an empty list `tphi_times` to store the Tphi times for each external flux in the circuit. b. Iterates over the external fluxes in the circuit. c. Calls the method for calculating Tphi due to 1/f flux noise for the current external flux. The method is an attribute of the current instance that is named "tphi_1_over_f_flux" followed by the trailing number in the name of the flux. d. Appends the calculated Tphi time to the `tphi_times` list. e. Calculates the total rate of dephasing by summing the reciprocals of the Tphi times. f. If the `get_rate` parameter is True, returns the total rate of dephasing. Otherwise, returns the reciprocal of the total rate (the overall Tphi time), or infinity if the total rate is zero. 3. Adds the `tphi_1_over_f_flux` method as an attribute of the current instance. This is done using the `setattr` function and the `MethodType` class to bind the method to the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the `tphi_1_over_f_flux` method as an attribute. """ if not any([re.match(r"tphi_1_over_f_ng\d+$", method) for method in dir(self)]): return None def tphi_1_over_f_ng( self=self, A_noise: float = NOISE_PARAMS["A_ng"], i: int = 0, j: int = 1, esys: Tuple[ndarray, ndarray] = None, get_rate: bool = False, **kwargs, ) -> float: r"""Calculate the 1/f dephasing time (or rate) due to charge noise. Parameters ---------- A_noise: noise strength i: int >=0 state index that along with j defines a qubit j: int >=0 state index that along with i defines a qubit esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ tphi_times = [] for flux_sym in self.offset_charges: tphi_times.append( getattr( self, f"tphi_1_over_f_ng{get_trailing_number(flux_sym.name)}" )( A_noise=A_noise, i=i, j=j, esys=esys, **kwargs, ) ) total_rate = sum([1 / tphi for tphi in tphi_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "tphi_1_over_f_ng", MethodType(tphi_1_over_f_ng, self)) def _t1_flux_bias_line_function_factory(self, noise_op_method: Callable): def flux_bias_noise( self=self, i: int = 1, j: int = 0, M: float = NOISE_PARAMS["M"], Z: Union[complex, float, Callable] = NOISE_PARAMS["R_0"], T: float = NOISE_PARAMS["T"], total: bool = True, esys: Tuple[ndarray, ndarray] = None, get_rate: bool = False, ): r"""Noise due to a bias flux line. References: Koch et al (2007), Groszkowski et al (2018) Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) M: Inductance in units of \Phi_0 / Ampere Z: A complex impedance; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ return NoisySystem.t1_flux_bias_line( self=self, i=i, j=j, M=M, Z=Z, T=T, total=total, esys=esys, get_rate=get_rate, noise_op_method=noise_op_method, ) return flux_bias_noise def _generate_t1_flux_bias_line_methods(self): """ T1 Coherence Time Calculator due to Flux Bias Line Noise -------------------------------------------------------- This function calculates the T1 coherence times due to flux bias line noise for each external flux in the quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Steps ----- 1. Initializes an empty dictionary `flux_bias_line_methods` to store the T1 calculation methods for each external flux. 2. Iterates over the external fluxes in the circuit. 3. Extracts the trailing number from the name of the current flux using the `get_trailing_number` function. 4. Retrieves the method for calculating the derivative of the Hamiltonian with respect to the current flux. This is done using the `getattr` function and the name of the method, which is "d_hamiltonian_d_flux" followed by the trailing number. 5. Defines a new method `flux_bias_noise` for calculating the T1 time due to flux bias line noise for the current flux. This method performs the following steps: a. Calls the `t1_flux_bias_line` method of the `NoisySystem` class with the current instance, the state indices `i` and `j`, the noise parameters `M`, `Z`, and `T`, the `total` flag, the system eigenvalues and eigenvectors `esys`, the `get_rate` flag, and the noise operator method as arguments. b. Returns the calculated T1 time. 6. Adds the `flux_bias_noise` method to the `flux_bias_line_methods` dictionary with a key that is "t1_flux_bias_line" followed by the trailing number. 7. Iterates over the keys in the `flux_bias_line_methods` dictionary. 8. Adds each method in the `flux_bias_line_methods` dictionary as an attribute of the current instance. This is done using the `setattr` function and the `MethodType` class to bind the method to the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the T1 calculation methods as attributes. """ flux_bias_line_methods = {} for flux_sym in self.external_fluxes: trailing_number = get_trailing_number(flux_sym.name) noise_op_method = getattr(self, f"d_hamiltonian_d_flux{trailing_number}") flux_bias_line_methods[f"t1_flux_bias_line{trailing_number}"] = ( self._t1_flux_bias_line_function_factory(noise_op_method) ) for method_name in flux_bias_line_methods: setattr( self, method_name, MethodType(flux_bias_line_methods[method_name], self) ) def _generate_t1_methods(self): """Generates methods for calculating the T1 coherence times due to capacitive, inductive, and charge impedance noise for each branch in the circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Notes ----- This function performs the following steps: 1. Initializes empty dictionaries `t1_capacitive_methods`, `t1_inductive_methods`, and `t1_charge_impedance_methods` to store the T1 calculation methods for each type of noise. 2. Iterates over the branches in the circuit. 3. Checks the type of the current branch. 4. If the branch type is "L", adds a method for calculating the T1 time due to inductive noise to the `t1_inductive_methods` dictionary. The method is generated by calling the `wrapper_t1_inductive_capacitive` function with the current branch as an argument. The key for the method in the dictionary is "t1_inductive" followed by the ID string of the branch. 5. If the branch type is not "L", adds a method for calculating the T1 time due to capacitive noise to the `t1_capacitive_methods` dictionary. The method is generated in the same way as for inductive noise. 6. Merges the `t1_capacitive_methods`, `t1_inductive_methods`, and `t1_charge_impedance_methods` dictionaries into a single `noise_methods` dictionary. 7. Iterates over the keys in the `noise_methods` dictionary. 8. Adds each method in the `noise_methods` dictionary as an attribute of the current instance. This is done using the `setattr` function and the `MethodType` class to bind the method to the current instance. This function does not return anything; it modifies the current instance by adding the T1 calculation methods as attributes. """ t1_capacitive_methods = {} t1_inductive_methods = {} t1_charge_impedance_methods = {} t1_quasiparticle_tunneling_methods = {} for branch in self.branches: if branch.type == "L": t1_inductive_methods[f"t1_inductive{branch.index}"] = ( self._wrapper_t1_inductive_capacitive(branch) ) else: t1_capacitive_methods[f"t1_capacitive{branch.index}"] = ( self._wrapper_t1_inductive_capacitive(branch) ) # # quasiparticle noise # if "JJ" in branch.type: # t1_quasiparticle_tunneling_methods[ # f"t1_quasiparticle_tunneling{branch.index}" # ] = self.wrapper_t1_quasiparticle_tunneling(branch) # quasiparticle noise methods are not included yet noise_methods = { **t1_capacitive_methods, **t1_inductive_methods, **t1_charge_impedance_methods, } for method_name in noise_methods: setattr(self, method_name, MethodType(noise_methods[method_name], self)) # self._data.update(t1_quasiparticle_tunneling_methods) def _wrapper_t1_quasiparticle_tunneling(self, branch: Branch): def t1_quasiparticle_tunneling( self=self, i: int = 1, j: int = 0, Y_qp: Optional[Union[float, Callable]] = None, x_qp: float = NOISE_PARAMS["x_qp"], T: float = NOISE_PARAMS["T"], Delta: float = NOISE_PARAMS["Delta"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r"""Noise due to quasiparticle tunneling across a Josephson junction. References: Smith et al (2020), Catelani et al (2011), Pop et al (2014). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Y_qp: complex admittance; a fixed value or function of `omega` x_qp: quasiparticle density (in units of eV) T: temperature in Kelvin Delta: superconducting gap (in units of eV) total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ return NoisySystem.t1_quasiparticle_tunneling( self=self, i=i, j=j, Y_qp=Y_qp, x_qp=x_qp, T=T, Delta=Delta, total=total, esys=esys, get_rate=get_rate, noise_op=self._junction_related_evaluation(branch, calc="sin_phi_qp"), ) return t1_quasiparticle_tunneling def _wrapper_t1_charge_impedance(self, branch: Branch): """Generates the t1_charge_impedance method necessary for a given impedance line, which couples to the circuit with a capacitance to one or a set of nodes. The input `node_expr`, is the symbolic expression for the nodes coupled to the impedance line. Parameters ---------- node_expr: The symbolic expression for the nodes coupled to the impedance line. """ # find the conjugate charge operator for the given node_expr def t1_charge_impedance( self, i: int = 1, j: int = 0, Z: Union[float, Callable] = NOISE_PARAMS["R_0"], T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r"""Noise due to charge coupling to an impedance (such as a transmission line). References: Schoelkopf et al (2003), Ithier et al (2005) Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Z: impedance; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ parent_circuit = self.return_parent_circuit() branch_var_expr = parent_circuit.symbolic_circuit._branch_sym_expr( branch, return_charge=False if branch.type == "L" else True ) if branch.type != "L": branch_param = ( branch.parameters["EC"] if branch.type == "C" else branch.parameters["ECJ"] ) else: branch_param = branch.parameters["EL"] if isinstance(branch_param, sm.Expr): branch_param = getattr(parent_circuit, branch_param.name) return NoisySystem.t1_charge_impedance( self=self, i=i, j=j, Z=Z, T=T, total=total, esys=esys, get_rate=get_rate, noise_op=parent_circuit._evaluate_symbolic_expr(branch_var_expr), ) return t1_charge_impedance def _wrapper_t1_inductive_capacitive( self, branch: Branch, ): """ T1 Coherence Time Calculator due to Inductive or Capacitive Noise ----------------------------------------------------------------- This function generates a method for calculating the T1 coherence time due to inductive or capacitive noise for a given branch in the quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. branch : Branch The branch of the circuit for which to calculate the T1 time. Returns ------- function The generated method for calculating the T1 time. Steps ----- 1. Checks the type of the branch. 2. If the branch type is not "L", generates a method for calculating the T1 time due to capacitive noise. 3. If the branch type is "L", generates a method for calculating the T1 time due to inductive noise by calling the `t1_inductive` method. 4. Returns the generated method. Notes ----- This function does not modify the current instance; it returns a new method for calculating the T1 time. """ if branch.type != "L": # inductive noise for any branch with a capacitance def t1_method( self, i: int = 1, j: int = 0, Q_cap: Optional[Union[float, Callable]] = None, T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, branch: Branch = branch, ) -> float: r""":math:`T_1` due to dielectric dissipation in the Josephson junction capacitances. References: Smith et al (2020), see also Nguyen et al (2019). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Q_cap capacitive quality factor; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ parent_circuit = self.return_parent_circuit() branch_charge_expr = ( parent_circuit.symbolic_circuit._branch_charge_expr(branch) ) branch_param = ( branch.parameters["EC"] if branch.type == "C" else branch.parameters["ECJ"] ) if isinstance(branch_param, sm.Expr): branch_param = getattr(parent_circuit, branch_param.name) return NoisySystem.t1_capacitive( self=self, i=i, j=j, Q_cap=Q_cap or self.Q_from_branch(branch), T=T, total=total, esys=esys, get_rate=get_rate, noise_op=parent_circuit._evaluate_symbolic_expr(branch_charge_expr), branch_params=branch_param, ) else: def t1_method( self, i: int = 1, j: int = 0, Q_ind: Optional[Union[float, Callable]] = None, T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, branch: Branch = branch, ) -> float: r""":math:`T_1` due to inductive dissipation in a superinductor. References: Smith et al (2020), see also Nguyen et al (2019). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Q_ind: inductive quality factor; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ parent_circuit = self.return_parent_circuit() branch_var_expr = parent_circuit.symbolic_circuit._branch_flux_expr( branch ) branch_param = branch.parameters["EL"] if isinstance(branch_param, sm.Expr): branch_param = getattr(parent_circuit, branch_param.name) return NoisySystem.t1_inductive( self=self, i=i, j=j, Q_ind=Q_ind or self.Q_from_branch(branch), T=T, total=total, esys=esys, get_rate=get_rate, noise_op=parent_circuit._evaluate_symbolic_expr(branch_var_expr), branch_params=branch_param, ) return t1_method def _generate_overall_t1_quasiparticle_tunneling(self): """ T1 Coherence Time Calculator due to Quasiparticle Tunneling ------------------------------------------------------------ This function generates an overall method for calculating the T1 coherence time due to quasiparticle tunneling for the entire circuit. Returns ------- None Description ----------- 1. Checks if there are any existing methods for calculating T1 due to quasiparticle tunneling for any of the branches in the circuit. If not, it returns None. 2. Checks if the circuit is purely harmonic, in which case it also returns None. 3. If the checks pass, it defines a new method `t1_quasiparticle_tunneling` that calculates the overall T1 time due to quasiparticle tunneling. 4. This method iterates over all the branches in the circuit that are Josephson junctions (indicated by "JJ" in the branch type), and for each branch, it calls the corresponding branch-specific T1 calculation method. 5. It then calculates the total rate of decoherence as the sum of the reciprocals of the T1 times for all the branches, and returns the reciprocal of the total rate (or the total rate itself if `get_rate` is True). 6. Finally, the function adds the `t1_quasiparticle_tunneling` method as an attribute of the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the T1 calculation methods as attributes. """ if not any( [ re.match(r"t1_quasiparticle_tunneling\d+$", method) for method in dir(self) ] ): return None if self.is_purely_harmonic: return None def t1_quasiparticle_tunneling( self=self, i: int = 1, j: int = 0, Y_qp: Optional[Union[float, Callable]] = None, x_qp: float = NOISE_PARAMS["x_qp"], T: float = NOISE_PARAMS["T"], Delta: float = NOISE_PARAMS["Delta"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r"""Overall T1 time due to quasiparticle tunneling across all the Josephson junctions in a circuit. References: Smith et al (2020), Catelani et al (2011), Pop et al (2014). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Y_qp: complex admittance; a fixed value or function of `omega` x_qp: quasiparticle density (in units of eV) T: temperature in Kelvin Delta: superconducting gap (in units of eV) total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ t1_times = [] for branch in [b for b in self.branches if "JJ" in b.type]: t1_times.append( getattr(self, f"t1_quasiparticle_tunneling{branch.index}")( i=i, j=j, Y_qp=Y_qp, x_qp=x_qp, T=T, Delta=Delta, total=total, esys=esys, ) ) total_rate = sum([1 / t1 for t1 in t1_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr( self, "t1_quasiparticle_tunneling", MethodType(t1_quasiparticle_tunneling, self), ) def _generate_overall_t1_inductive(self): """ T1 Coherence Time Calculator due to Inductive Noise --------------------------------------------------- This function generates an overall method for calculating the T1 coherence time due to inductive noise for the entire circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Description ----------- 1. Checks if there are any existing methods for calculating T1 due to inductive noise for any of the branches in the circuit. If not, it returns None. 2. If the checks pass, it defines a new method `t1_method` that calculates the overall T1 time due to inductive noise. 3. This method iterates over all the branches in the circuit that are inductors (indicated by "L" in the branch type), and for each branch, it calls the corresponding branch-specific T1 calculation method. 4. It then calculates the total rate of decoherence as the sum of the reciprocals of the T1 times for all the branches, and returns the reciprocal of the total rate (or the total rate itself if `get_rate` is True). 5. Finally, the function adds the `t1_method` method as an attribute of the current instance. Notes: ------ This function does not return anything; it modifies the current instance by adding the T1 calculation methods as attributes. """ if not any([re.match(r"t1_inductive\d+$", method) for method in dir(self)]): return None def t1_method( self, i: int = 1, j: int = 0, Q_ind: Optional[Union[float, Callable]] = None, T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r""":math:`T_1` due to inductive dissipation in all the superinductors of the circuit. References: Smith et al (2020), see also Nguyen et al (2019). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Q_ind: inductive quality factor; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ t1_times = [] parent_circuit = self.return_parent_circuit() for branch in [b for b in parent_circuit.branches if b.type == "L"]: t1_times.append( getattr(parent_circuit, f"t1_inductive{branch.index}")( i=i, j=j, Q_ind=Q_ind or self.Q_from_branch(branch), T=T, total=total, esys=esys, ) ) total_rate = sum([1 / t1 for t1 in t1_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "t1_inductive", MethodType(t1_method, self)) def _generate_overall_t1_capacitive(self): """ T1 Coherence Time Calculator due to Capacitive Noise ---------------------------------------------------- This function generates an overall method for calculating the T1 coherence time due to capacitive noise for the entire circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Description ----------- 1. Checks if there are any existing methods for calculating T1 due to capacitive noise for any of the branches in the circuit. If not, it returns None. 2. If the checks pass, it defines a new method `t1_method` that calculates the overall T1 time due to capacitive noise. 3. This method iterates over all the branches in the circuit that are not inductors (indicated by "L" in the branch type), and for each branch, it calls the corresponding branch-specific T1 calculation method. 4. It then calculates the total rate of decoherence as the sum of the reciprocals of the T1 times for all the branches, and returns the reciprocal of the total rate (or the total rate itself if `get_rate` is True). 5. Finally, the function adds the `t1_method` method as an attribute of the current instance. Notes ----- This function does not return anything; it modifies the current instance by adding the T1 calculation methods as attributes. """ if not any([re.match(r"t1_capacitive\d+$", method) for method in dir(self)]): return None def t1_method( self, i: int = 1, j: int = 0, Q_cap: Optional[Union[float, Callable]] = None, T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r""":math:`T_1` due to dielectric dissipation in the all the capacitances of the circuit. References: Smith et al (2020), see also Nguyen et al (2019). Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Q_cap capacitive quality factor; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ t1_times = [] parent_circuit = self.return_parent_circuit() for branch in [b for b in parent_circuit.branches if b.type != "L"]: t1_times.append( getattr(parent_circuit, f"t1_capacitive{branch.index}")( i=i, j=j, Q_cap=Q_cap or self.Q_from_branch(branch), T=T, total=total, esys=esys, ) ) total_rate = sum([1 / t1 for t1 in t1_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "t1_capacitive", MethodType(t1_method, self)) def _generate_overall_t1_charge_impedance(self): """ This function dynamically generates a method for calculating the T1 coherence time due to charge impedance noise for the entire quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Notes ----- This function performs the following steps: 1. Checks if there are any existing methods for calculating T1 due to charge impedance noise for any of the branches in the circuit. If not, it returns None. 2. Defines a new method `t1_method` that calculates the overall T1 time due to charge impedance noise. This method: - Iterates over all the branches in the circuit that are not inductors (indicated by "L" in the branch type). - For each branch, it calls the corresponding branch-specific T1 calculation method. - Calculates the total rate of decoherence as the sum of the reciprocals of the T1 times for all the branches. - Returns the reciprocal of the total rate (or the total rate itself if `get_rate` is True). 3. Adds the `t1_method` method as an attribute of the current instance. This function does not return anything; it modifies the current instance by adding the T1 calculation method as an attribute. """ if not any( [re.match(r"t1_charge_impedance\d+$", method) for method in dir(self)] ): return None def t1_method( self=self, i: int = 1, j: int = 0, Z: Union[float, Callable] = NOISE_PARAMS["R_0"], T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r"""Noise due to all the charge couplings to impedances (such as a transmission line) in a circuit. References: Schoelkopf et al (2003), Ithier et al (2005) Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) Z: impedance; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ t1_times = [] parent_circuit = self.return_parent_circuit() for branch in [b for b in parent_circuit.branches if b.type != "L"]: t1_times.append( getattr(parent_circuit, f"t1_charge_impedance{branch.index}")( i=i, j=j, Z=Z, T=T, total=total, esys=esys, ) ) total_rate = sum([1 / t1 for t1 in t1_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "t1_charge_impedance", MethodType(t1_method, self)) def _generate_overall_t1_flux_bias_line(self): """ This function dynamically generates a method for calculating the T1 coherence time due to flux bias line noise for the entire quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Notes ----- This function performs the following steps: 1. Checks if there are any existing methods for calculating T1 due to flux bias line noise for any of the branches in the circuit. If not, it returns None. 2. Defines a new method `t1_flux_bias_line` that calculates the overall T1 time due to flux bias line noise. This method: - Iterates over all the external fluxes in the circuit. - For each flux, it calls the corresponding branch-specific T1 calculation method. - Calculates the total rate of decoherence as the sum of the reciprocals of the T1 times for all the fluxes. - Returns the reciprocal of the total rate (or the total rate itself if `get_rate` is True). 3. Adds the `t1_flux_bias_line` method as an attribute of the current instance. This function does not return anything; it modifies the current instance by adding the T1 calculation method as an attribute. """ if not any( [re.match(r"t1_flux_bias_line\d+$", method) for method in dir(self)] ): return None def t1_flux_bias_line( self=self, i: int = 1, j: int = 0, M: float = NOISE_PARAMS["M"], Z: Union[complex, float, Callable] = NOISE_PARAMS["R_0"], T: float = NOISE_PARAMS["T"], total: bool = True, esys: Optional[Tuple[ndarray, ndarray]] = None, get_rate: bool = False, ) -> float: r"""Total Noise due to all flux bias lines in a circuit. References: Koch et al (2007), Groszkowski et al (2018) Parameters ---------- i: int >=0 state index that along with j defines a transition (i->j) j: int >=0 state index that along with i defines a transition (i->j) M: Inductance in units of \Phi_0 / Ampere Z: A complex impedance; a fixed value or function of `omega` T: temperature in Kelvin total: if False return a time/rate associated with a transition from state i to state j. if True return a time/rate associated with both i to j and j to i transitions esys: evals, evecs tuple get_rate: get rate or time Returns ------- time or rate: float decoherence time in units of :math:`2\pi ({\rm system\,\,units})`, or rate in inverse units. """ t1_times = [] for external_flux_sym in self.external_fluxes: t1_times.append( getattr( self, f"t1_flux_bias_line{get_trailing_number(external_flux_sym.name)}", )( i=i, j=j, M=M, Z=Z, T=T, total=total, esys=esys, ) ) total_rate = sum([1 / t1 for t1 in t1_times]) if get_rate: return total_rate return 1 / total_rate if total_rate != 0 else np.inf setattr(self, "t1_flux_bias_line", MethodType(t1_flux_bias_line, self))
[docs] def generate_noise_methods(self): """This function is responsible for dynamically generating all the methods that calculate the different types of noise in the quantum circuit. Parameters ---------- self : object The instance of the class where this method is being added. Returns ------- None Notes ----- This function performs the following steps: 1. Unfreezes the circuit to allow changes. 2. Generates methods for calculating the derivatives of the Hamiltonian with respect to the different circuit parameters. 3. Generates methods for calculating the Tphi times due to 1/f noise for each branch in the circuit. 4. Generates methods for calculating the T1 times due to flux bias line noise for each branch in the circuit. 5. Generates methods for calculating the T1 times due to various noise sources for each branch in the circuit. 6. Generates methods for calculating the overall Tphi times due to charge coupling, flux, and ng noise for the entire circuit. 7. Generates methods for calculating the overall T1 times due to capacitive, charge impedance, inductive, flux bias line, and quasiparticle tunneling noise for the entire circuit. 8. Sets the `_noise_methods_generated` attribute to True. 9. Freezes the circuit again to prevent further changes. The methods generated by this function are: - `generate_methods_d_hamiltonian_d` - `generate_tphi_1_over_f_methods` - `generate_t1_flux_bias_line_methods` - `generate_t1_methods` - `generate_overall_tphi_cc` - `generate_overall_tphi_flux` - `generate_overall_tphi_ng` - `generate_overall_t1_capacitive` - `generate_overall_t1_charge_impedance` - `generate_overall_t1_inductive` - `generate_overall_t1_flux_bias_line` - `generate_overall_t1_quasiparticle_tunneling` This function does not return anything; it modifies the current instance by adding the noise calculation methods as attributes. """ self._frozen = False self._generate_methods_d_hamiltonian_d() self._generate_tphi_1_over_f_methods() self._generate_t1_flux_bias_line_methods() self._generate_t1_methods() self._generate_overall_tphi_cc() self._generate_overall_tphi_flux() self._generate_overall_tphi_ng() self._generate_overall_t1_capacitive() self._generate_overall_t1_charge_impedance() self._generate_overall_t1_inductive() self._generate_overall_t1_flux_bias_line() self._generate_overall_t1_quasiparticle_tunneling() self._noise_methods_generated = True self._frozen = True