# circuit.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.
############################################################################
import re
import warnings
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
import numpy as np
import sympy as sm
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from numpy import ndarray
from sympy import latex
try:
from IPython.display import Latex, display
except ImportError:
_HAS_IPYTHON = False
else:
_HAS_IPYTHON = True
import scqubits.core.discretization as discretization
import scqubits.core.central_dispatch as dispatch
import scqubits.core.qubit_base as base
import scqubits.io_utils.fileio_serializers as serializers
from scqubits import settings
from scqubits.core.circuit_utils import (
get_trailing_number,
is_potential_term,
)
from scqubits.core.symbolic_circuit import Branch, SymbolicCircuit
from scqubits.utils.misc import (
flatten_list,
flatten_list_recursive,
is_string_float,
number_of_lists_in_list,
)
from scqubits.core.circuit_routines import CircuitRoutines
from scqubits.core.circuit_noise import NoisyCircuit
[docs]class Subsystem(
CircuitRoutines,
base.QubitBaseClass,
serializers.Serializable,
dispatch.DispatchClient,
NoisyCircuit,
):
"""
Defines a subsystem for a circuit, which can further be used recursively to define
subsystems within subsystem.
Parameters
----------
parent: Subsystem
the instance under which the new subsystem is defined.
ext_basis: str
The basis that should be used for extended variables
hamiltonian_symbolic: sm.Expr
The symbolic expression which defines the Hamiltonian for the new subsystem
system_hierarchy: Optional[List], optional
Defines the hierarchy of the new subsystem, is set to None when hierarchical
diagonalization is not required. by default None
subsystem_trunc_dims: Optional[List], optional
Defines the truncated dimensions for the subsystems inside the current
subsystem, is set to None when hierarchical diagonalization is not required,
by default `None`
truncated_dim: Optional[int], optional
sets the truncated dimension for the current subsystem, set to 10 by default.
"""
def __init__(
self,
parent: "Subsystem",
hamiltonian_symbolic: sm.Expr,
ext_basis: Union[str, List],
system_hierarchy: Optional[List] = None,
subsystem_trunc_dims: Optional[List] = None,
truncated_dim: Optional[int] = 10,
evals_method: Union[Callable, str, None] = None,
evals_method_options: Union[dict, None] = None,
esys_method: Union[Callable, str, None] = None,
esys_method_options: Union[dict, None] = None,
):
# switch used in protecting the class from erroneous addition of new attributes
object.__setattr__(self, "_frozen", False)
base.QubitBaseClass.__init__(
self,
id_str=None,
evals_method=evals_method,
evals_method_options=evals_method_options,
esys_method=esys_method,
esys_method_options=esys_method_options,
)
self.system_hierarchy = system_hierarchy
self.truncated_dim = truncated_dim
self.subsystem_trunc_dims = subsystem_trunc_dims
self.is_child = True
self.parent = parent
self.hamiltonian_symbolic = hamiltonian_symbolic
self._default_grid_phi = self.parent._default_grid_phi
self.junction_potential = None
self._H_LC_str_harmonic = None
# attribute to keep track if the symbolic Hamiltonian needs to be updated
self._make_property(
"_user_changed_parameter",
False,
"update_user_changed_parameter",
use_central_dispatch=False,
)
self.ext_basis = ext_basis
self._find_and_set_sym_attrs()
self.dynamic_var_indices: List[int] = flatten_list_recursive(
[self.system_hierarchy]
)
parent_cutoffs_dict = self.parent.cutoffs_dict()
cutoffs: List[int] = [
parent_cutoffs_dict[var_index] for var_index in self.dynamic_var_indices
]
self.var_categories: Dict[str, List[int]] = {}
for var_type in self.parent.var_categories:
self.var_categories[var_type] = [
var_index
for var_index in self.parent.var_categories[var_type]
if var_index in self.dynamic_var_indices
]
self.cutoff_names: List[str] = []
for var_type in self.var_categories.keys():
if var_type == "periodic":
for var_index in self.var_categories["periodic"]:
self.cutoff_names.append(f"cutoff_n_{var_index}")
if var_type == "extended":
for var_index in self.var_categories["extended"]:
self.cutoff_names.append(f"cutoff_ext_{var_index}")
self.discretized_phi_range: Dict[int, Tuple[float]] = {
idx: self.parent.discretized_phi_range[idx]
for idx in self.parent.discretized_phi_range
if idx in self.dynamic_var_indices
}
# storing the potential terms separately
self.potential_symbolic = self.generate_sym_potential()
self.hierarchical_diagonalization: bool = (
system_hierarchy != [] and number_of_lists_in_list(system_hierarchy) > 0
)
if len(self.dynamic_var_indices) == 1:
self.type_of_matrices = "dense"
else:
self.type_of_matrices = "sparse"
# needs to be included to make sure that plot_evals_vs_paramvals works
self._init_params = []
# attributes for purely harmonic
self.normal_mode_freqs = []
self._configure()
self._frozen = True
def _find_and_set_sym_attrs(self):
"""
Finds the symbolic and other circuit params from the symbolic Hamiltonian, and sets the attribs
external_fluxes, offset_charges and symbolic_params. Only works when _frozen is set to False, or
the above attribs are already set.
"""
self.external_fluxes = [
var
for var in self.parent.external_fluxes
if var in self.hamiltonian_symbolic.free_symbols
]
self.offset_charges = [
var
for var in self.parent.offset_charges
if var in self.hamiltonian_symbolic.free_symbols
]
self.free_charges = [
var
for var in self.parent.free_charges
if var in self.hamiltonian_symbolic.free_symbols
]
self.symbolic_params = {
var: self.parent.symbolic_params[var]
for var in self.parent.symbolic_params
if var in self.hamiltonian_symbolic.free_symbols
}
def _configure(self) -> None:
"""
Function which is used to initiate the subsystem instance.
"""
self._frozen = False
for idx, param in enumerate(self.symbolic_params):
self._make_property(
param.name, getattr(self.parent, param.name), "update_param_vars"
)
# getting attributes from parent
for flux in self.external_fluxes:
self._make_property(
flux.name,
getattr(self.parent, flux.name),
"update_external_flux_or_charge",
)
for charge_var in self.offset_charges + self.free_charges:
self._make_property(
charge_var.name,
getattr(self.parent, charge_var.name),
"update_external_flux_or_charge",
)
for cutoff_str in self.cutoff_names:
self._make_property(
cutoff_str, getattr(self.parent, cutoff_str), "update_cutoffs"
)
# if subsystem hamiltonian is purely harmonic
if (
self._is_expression_purely_harmonic(self.hamiltonian_symbolic)
and self.ext_basis == "harmonic"
):
self.is_purely_harmonic = True
self._annihilation_operator_in_eigenbasis = None
else:
self.is_purely_harmonic = False
# Creating the attributes for purely harmonic circuits
if (
isinstance(self, Circuit) and self.parent.is_purely_harmonic
): # assuming that the parent has only extended variables and are ordered
# starting from 1, 2, 3, ...
self.is_purely_harmonic = self.parent.is_purely_harmonic
self.normal_mode_freqs = self.parent.normal_mode_freqs[
[var_idx - 1 for var_idx in self.var_categories["extended"]]
]
if self.hierarchical_diagonalization:
# attribute to note updated subsystem indices
self.affected_subsystem_indices = []
self._hamiltonian_sym_for_numerics = self.hamiltonian_symbolic.copy()
self.generate_subsystems()
self.ext_basis = self.get_ext_basis()
self.update_interactions()
self._check_truncation_indices()
self.affected_subsystem_indices = list(range(len(self.subsystems)))
else:
self.generate_hamiltonian_sym_for_numerics()
if self.is_purely_harmonic and self.ext_basis == "harmonic":
self._diagonalize_purely_harmonic_hamiltonian()
self._set_vars()
self.operators_by_name = self.set_operators()
if self.hierarchical_diagonalization:
self._out_of_sync = False # for use with CentralDispatch
dispatch.CENTRAL_DISPATCH.register("CIRCUIT_UPDATE", self)
self._frozen = True
[docs]class Circuit(
CircuitRoutines,
base.QubitBaseClass,
serializers.Serializable,
dispatch.DispatchClient,
NoisyCircuit,
):
"""
Class for analysis of custom superconducting circuits.
Parameters
----------
input_string: str
String describing the number of nodes and branches connecting then along
with their parameters
from_file: bool
Set to True by default, when a file name should be provided to
`input_string`, else the circuit graph description in YAML should be
provided as a string.
basis_completion: str
either "heuristic" or "canonical", defines the matrix used for completing the
transformation matrix. Sometimes used to change the variable transformation
to result in a simpler symbolic Hamiltonian, by default "heuristic"
ext_basis: str
can be "discretized" or "harmonic" which chooses whether to use discretized
phi or harmonic oscillator basis for extended variables,
by default "discretized"
use_dynamic_flux_grouping: bool
set to False by default. Indicates if the flux allocation is done by assuming
that flux is time dependent. When set to True, it disables the option to change
the closure branches.
initiate_sym_calc: bool
attribute to initiate Circuit instance, by default `True`
truncated_dim: Optional[int]
truncated dimension if the user wants to use this circuit instance in
HilbertSpace, by default `None`
"""
def __init__(
self,
input_string: Optional[str] = None,
from_file: bool = True,
basis_completion="heuristic",
ext_basis: str = "discretized",
use_dynamic_flux_grouping: bool = False,
generate_noise_methods: bool = False,
initiate_sym_calc: bool = True,
truncated_dim: int = 10,
symbolic_param_dict: Dict[str, float] = None,
symbolic_hamiltonian: sm.Expr = None,
evals_method: Union[Callable, str, None] = None,
evals_method_options: Union[dict, None] = None,
esys_method: Union[Callable, str, None] = None,
esys_method_options: Union[dict, None] = None,
):
# switch used in protecting the class from erroneous addition of new attributes
object.__setattr__(self, "_frozen", False)
base.QubitBaseClass.__init__(
self,
id_str=None,
evals_method=evals_method,
evals_method_options=evals_method_options,
esys_method=esys_method,
esys_method_options=esys_method_options,
)
if symbolic_hamiltonian and input_string:
raise Exception(
"Circuit instance cannot be initialized with both input_string and symbolic_hamiltonian."
)
if input_string:
self.from_yaml(
input_string=input_string,
from_file=from_file,
basis_completion=basis_completion,
ext_basis=ext_basis,
use_dynamic_flux_grouping=use_dynamic_flux_grouping,
generate_noise_methods=generate_noise_methods,
initiate_sym_calc=initiate_sym_calc,
truncated_dim=truncated_dim,
)
else:
if use_dynamic_flux_grouping or generate_noise_methods:
raise Exception(
"Circuit instance initialized using symbolic Hamiltonian cannot be configured with closure_branches, use_dynamic_flux_grouping, transformation_matrix or generate_noise_methods."
)
self.from_symbolic_hamiltonian(
symbolic_hamiltonian=symbolic_hamiltonian,
symbolic_param_dict=symbolic_param_dict,
initiate_sym_calc=initiate_sym_calc,
truncated_dim=truncated_dim,
ext_basis=ext_basis,
)
def from_symbolic_hamiltonian(
self,
symbolic_hamiltonian: sm.Expr,
symbolic_param_dict: Dict[str, float],
initiate_sym_calc: bool,
truncated_dim: int,
ext_basis: str,
):
self.hamiltonian_symbolic = symbolic_hamiltonian
self.symbolic_params = {}
for param_str in symbolic_param_dict:
if "ng" in param_str or "Φ" in param_str:
continue
self.symbolic_params[sm.symbols(param_str)] = symbolic_param_dict[param_str]
sm.init_printing(pretty_print=False, order="none")
self.is_child = False
self.ext_basis = ext_basis
self.truncated_dim: int = truncated_dim
self.system_hierarchy: list = None
self.subsystem_trunc_dims: list = None
self.operators_by_name = None
self.discretized_phi_range: Dict[int, Tuple[float, float]] = {}
self.cutoff_names: List[str] = []
# setting default grids for plotting
self._default_grid_phi: discretization.Grid1d = discretization.Grid1d(
-6 * np.pi, 6 * np.pi, 200
)
self.type_of_matrices: str = (
"sparse" # type of matrices used to construct the operators
)
# needs to be included to make sure that plot_evals_vs_paramvals works
self._init_params = []
self._out_of_sync = False # for use with CentralDispatch
self._make_property(
"_user_changed_parameter",
False,
"update_user_changed_parameter",
use_central_dispatch=False,
)
if initiate_sym_calc:
self.configure()
self._frozen = True
dispatch.CENTRAL_DISPATCH.register("CIRCUIT_UPDATE", self)
[docs] def from_yaml(
self,
input_string: str,
from_file: bool = True,
basis_completion="heuristic",
ext_basis: str = "discretized",
use_dynamic_flux_grouping: bool = False,
generate_noise_methods: bool = False,
initiate_sym_calc: bool = True,
truncated_dim: int = None,
):
"""
Wrapper to Circuit __init__ to create a class instance. This is deprecated and
will not be supported in future releases.
Parameters
----------
input_string:
String describing the number of nodes and branches connecting then along
with their parameters
from_file:
Set to True by default, when a file name should be provided to
`input_string`, else the circuit graph description in YAML should be
provided as a string.
basis_completion:
either "heuristic" or "canonical", defines the matrix used for completing
the transformation matrix. Sometimes used to change the variable
transformation to result in a simpler symbolic Hamiltonian, by default
"heuristic"
ext_basis:
can be "discretized" or "harmonic" which chooses whether to use discretized
phi or harmonic oscillator basis for extended variables,
by default "discretized"
initiate_sym_calc:
attribute to initiate Circuit instance, by default `True`
truncated_dim:
truncated dimension if the user wants to use this circuit instance in
HilbertSpace, by default `None`
use_dynamic_flux_grouping: bool
set to False by default. Indicates if the flux allocation is done by assuming that flux is time dependent. When set to True, it disables the
option to change the closure branches.
"""
if basis_completion not in ["heuristic", "canonical"]:
raise Exception(
"Invalid choice for basis_completion: must be 'heuristic' or "
"'canonical'."
)
symbolic_circuit = SymbolicCircuit.from_yaml(
input_string,
from_file=from_file,
basis_completion=basis_completion,
initiate_sym_calc=True,
use_dynamic_flux_grouping=use_dynamic_flux_grouping,
)
sm.init_printing(pretty_print=False, order="none")
self.is_child = False
self.symbolic_circuit: SymbolicCircuit = symbolic_circuit
self.ext_basis = ext_basis
self.hierarchical_diagonalization: bool = False
self.truncated_dim: int = truncated_dim
self.system_hierarchy: list = None
self.subsystem_trunc_dims: list = None
self.operators_by_name = None
self.discretized_phi_range: Dict[int, Tuple[float, float]] = {}
self.cutoff_names: List[str] = []
# setting default grids for plotting
self._default_grid_phi: discretization.Grid1d = discretization.Grid1d(
-6 * np.pi, 6 * np.pi, 200
)
self.type_of_matrices: str = (
"sparse" # type of matrices used to construct the operators
)
# copying all the required attributes
required_attributes = [
"branches",
"closure_branches",
"external_fluxes",
"ground_node",
"hamiltonian_symbolic",
"input_string",
"is_grounded",
"lagrangian_node_vars",
"lagrangian_symbolic",
"nodes",
"offset_charges",
"free_charges",
"potential_symbolic",
"potential_node_vars",
"symbolic_params",
"transformation_matrix",
"var_categories",
]
for attr in required_attributes:
setattr(self, attr, getattr(self.symbolic_circuit, attr))
# needs to be included to make sure that plot_evals_vs_paramvals works
self._init_params = []
self._out_of_sync = False # for use with CentralDispatch
self._make_property(
"_user_changed_parameter",
False,
"update_user_changed_parameter",
use_central_dispatch=False,
)
if initiate_sym_calc:
self.configure()
if generate_noise_methods:
self.generate_noise_methods()
self._frozen = True
dispatch.CENTRAL_DISPATCH.register("CIRCUIT_UPDATE", self)
def _find_branch(
self, node_id_1: int, node_id_2: int, branch_type: str, branch_params: dict
):
for branch in self.symbolic_circuit.branches:
branch_node_ids = [node.index for node in branch.nodes]
branch_params_circ = branch.parameters.copy()
for param in branch_params_circ:
if isinstance(branch_params_circ[param], sm.Symbol):
branch_params_circ[param] = branch_params_circ[param].name
if node_id_1 not in branch_node_ids or node_id_2 not in branch_node_ids:
continue
if branch.type != branch_type:
continue
if branch_params != branch_params_circ:
continue
return branch
return None
[docs] @staticmethod
def default_params() -> Dict[str, Any]:
return {}
def _clear_unnecessary_attribs(self):
"""
Clear all the attributes which are not part of the circuit description
"""
necessary_attrib_names = (
self.cutoff_names
+ [flux_symbol.name for flux_symbol in self.external_fluxes]
+ [
charge_symbol.name
for charge_symbol in self.offset_charges + self.free_charges
]
+ ["cutoff_names"]
)
attrib_keys = list(self.__dict__.keys()).copy()
for attrib in attrib_keys:
if attrib[1:] not in necessary_attrib_names:
if (
"cutoff_n_" in attrib
or "Φ" in attrib
or "cutoff_ext_" in attrib
or attrib[1:3] == "ng"
):
delattr(self, attrib)
def _read_symbolic_hamiltonian(
self, symbolic_hamiltonian: sm.Expr
) -> Tuple[List[sm.Expr], List[sm.Expr], List[sm.Expr], Dict[str, List[int]]]:
free_symbols = symbolic_hamiltonian.free_symbols
external_fluxes = []
offset_charges = []
free_charges = []
var_categories = {"periodic": [], "extended": [], "free": [], "frozen": []}
for var_sym in free_symbols:
if re.match(r"^ng\d+$", var_sym.name):
offset_charges.append(var_sym)
elif re.match(r"^Qf\d+$", var_sym.name):
free_charges.append(var_sym)
elif re.match(r"^Φ\d+$", var_sym.name):
external_fluxes.append(var_sym)
elif re.match(r"^n\d+$", var_sym.name):
var_index = get_trailing_number(var_sym.name)
var_categories["periodic"].append(var_index)
elif re.match(r"^Q\d+$", var_sym.name):
var_index = get_trailing_number(var_sym.name)
var_categories["extended"].append(var_index)
var_categories = {
category: sorted(var_categories[category]) for category in var_categories
}
return external_fluxes, offset_charges, free_charges, var_categories
def _configure_sym_hamiltonian(
self,
system_hierarchy: Optional[list] = None,
subsystem_trunc_dims: Optional[list] = None,
ext_basis: Optional[str] = None,
):
"""
Method which re-initializes a circuit instance to update, hierarchical
diagonalization parameters or closure branches or the variable transformation
used to describe the circuit.
Parameters
----------
system_hierarchy:
A list of lists which is provided by the user to define subsystems,
by default `None`
subsystem_trunc_dims:
dict object which can be generated for a specific system_hierarchy using the
method `truncation_template`, by default `None`
ext_basis:
can be "discretized" or "harmonic" which chooses whether to use discretized
phi or harmonic oscillator basis for extended variables,
by default `None`
Raises
------
Exception
when system_hierarchy is set and subsystem_trunc_dims is not set.
"""
self._frozen = False
system_hierarchy = system_hierarchy or self.system_hierarchy
subsystem_trunc_dims = subsystem_trunc_dims or self.subsystem_trunc_dims
self.ext_basis = ext_basis or self.ext_basis
self.hierarchical_diagonalization = (
True if system_hierarchy is not None else False
)
self.is_purely_harmonic = self._is_expression_purely_harmonic(
self.hamiltonian_symbolic
)
(
self.external_fluxes,
self.offset_charges,
self.free_charges,
self.var_categories,
) = self._read_symbolic_hamiltonian(self.hamiltonian_symbolic)
# initiating the class properties
self.cutoff_names = []
for var_type in self.var_categories.keys():
if var_type == "periodic":
for idx, var_index in enumerate(self.var_categories["periodic"]):
if not hasattr(self, f"_cutoff_n_{var_index}"):
self._make_property(
f"cutoff_n_{var_index}", 5, "update_cutoffs"
)
self.cutoff_names.append(f"cutoff_n_{var_index}")
if var_type == "extended":
for idx, var_index in enumerate(self.var_categories["extended"]):
if not hasattr(self, f"_cutoff_ext_{var_index}"):
self._make_property(
f"cutoff_ext_{var_index}", 30, "update_cutoffs"
)
self.cutoff_names.append(f"cutoff_ext_{var_index}")
self.dynamic_var_indices = (
self.var_categories["periodic"] + self.var_categories["extended"]
)
# default values for the parameters
for idx, param in enumerate(self.symbolic_params):
if not hasattr(self, param.name):
self._make_property(
param.name, self.symbolic_params[param], "update_param_vars"
)
# setting the ranges for flux ranges used for discrete phi vars
for var_index in self.var_categories["extended"]:
if var_index not in self.discretized_phi_range:
self.discretized_phi_range[var_index] = (-6 * np.pi, 6 * np.pi)
# external flux vars
for flux in self.external_fluxes:
# setting the default to zero external flux
if not hasattr(self, flux.name):
self._make_property(flux.name, 0.0, "update_external_flux_or_charge")
# offset charges
for charge_var in self.offset_charges + self.free_charges:
# default to zero offset charge
if not hasattr(self, charge_var.name):
self._make_property(
charge_var.name, 0.0, "update_external_flux_or_charge"
)
self.potential_symbolic = self.generate_sym_potential()
# changing the matrix type if necessary
if len(flatten_list(self.var_categories.values())) == 1:
self.type_of_matrices = "dense"
if system_hierarchy is not None:
self.hierarchical_diagonalization = (
system_hierarchy != [] and number_of_lists_in_list(system_hierarchy) > 0
)
if not self.hierarchical_diagonalization:
if self.is_purely_harmonic and not ext_basis:
if self.ext_basis != "harmonic":
warnings.warn(
"Purely harmonic circuits need ext_basis to be set to 'harmonic'"
)
self.ext_basis = "harmonic"
self.ext_basis = ext_basis or self.ext_basis
self.generate_hamiltonian_sym_for_numerics()
if self.is_purely_harmonic and self.ext_basis == "harmonic":
# using the default methods
self.evals_method = None
self.evals_method_options = None
self._annihilation_operator_in_eigenbasis = None
self._diagonalize_purely_harmonic_hamiltonian()
self._set_vars() # setting the attribute vars to store operator symbols
self.operators_by_name = self.set_operators()
else:
# list for updating necessary subsystems when calling build hilbertspace
self.affected_subsystem_indices = []
self.operators_by_name = None
self.system_hierarchy = system_hierarchy
if subsystem_trunc_dims is None:
raise Exception(
"The truncated dimensions attribute for hierarchical "
"diagonalization is not set."
)
self.subsystem_trunc_dims = subsystem_trunc_dims
if ext_basis:
self.ext_basis = ext_basis
self.generate_hamiltonian_sym_for_numerics()
self.generate_subsystems()
self.ext_basis = (
self.get_ext_basis()
) # update the ext_basis after generating subsystems
self._set_vars() # setting the attribute vars to store operator symbols
self._check_truncation_indices()
self.operators_by_name = self.set_operators()
self.affected_subsystem_indices = list(range(len(self.subsystems)))
self.update_interactions()
# clear unnecessary attribs
self._clear_unnecessary_attribs()
self._frozen = True
self.update()
def _configure(
self,
transformation_matrix: Optional[ndarray] = None,
system_hierarchy: Optional[list] = None,
subsystem_trunc_dims: Optional[list] = None,
closure_branches: Optional[List[Branch]] = None,
ext_basis: Optional[str] = None,
use_dynamic_flux_grouping: Optional[bool] = None,
generate_noise_methods: bool = False,
):
"""
Method which re-initializes a circuit instance to update, hierarchical
diagonalization parameters or closure branches or the variable transformation
used to describe the circuit.
Parameters
----------
transformation_matrix:
A user defined variable transformation which has the dimensions of the
number nodes (not counting the ground node), by default `None`
system_hierarchy:
A list of lists which is provided by the user to define subsystems,
by default `None`
subsystem_trunc_dims:
dict object which can be generated for a specific system_hierarchy using the
method `truncation_template`, by default `None`
closure_branches:
List of branches where external flux variables will be specified, by default
`None` which then chooses closure branches by an internally generated
spanning tree.
ext_basis:
can be "discretized" or "harmonic" which chooses whether to use discretized, or can be a list of lists of lists, when hierarchical diagonalization is used.
use_dynamic_flux_grouping:
set to False by default. Indicates if the flux allocation is done by assuming that flux is time dependent. When set to True, it disables the option to change the closure branches.
generate_noise_methods:
set to False by default. Indicates if the noise methods should be generated for the circuit instance.
Raises
------
Exception
when system_hierarchy is set and subsystem_trunc_dims is not set.
"""
self._frozen = False
# reinitiate the symbolic circuit when the transformation matrix and closure branches are provided
if (
transformation_matrix is not None
or closure_branches is not None
or use_dynamic_flux_grouping is not None
):
self.symbolic_circuit.configure(
transformation_matrix=transformation_matrix,
closure_branches=closure_branches,
use_dynamic_flux_grouping=use_dynamic_flux_grouping,
)
system_hierarchy = system_hierarchy or self.system_hierarchy
subsystem_trunc_dims = subsystem_trunc_dims or self.subsystem_trunc_dims
closure_branches = closure_branches or self.closure_branches
if transformation_matrix is None:
if hasattr(
self, "transformation_matrix"
): # checking to see if configure is being called outside of init
transformation_matrix = self.transformation_matrix
self.hierarchical_diagonalization = (
True if system_hierarchy is not None else False
)
# copying all the required attributes
required_attributes = [
"branches",
"closure_branches",
"external_fluxes",
"ground_node",
"hamiltonian_symbolic",
"input_string",
"is_grounded",
"lagrangian_node_vars",
"lagrangian_symbolic",
"nodes",
"offset_charges",
"free_charges",
"potential_symbolic",
"potential_node_vars",
"symbolic_params",
"transformation_matrix",
"var_categories",
"is_purely_harmonic",
]
for attr in required_attributes:
setattr(self, attr, getattr(self.symbolic_circuit, attr))
# initiating the class properties
self.cutoff_names = []
for var_type in self.var_categories.keys():
if var_type == "periodic":
for idx, var_index in enumerate(self.var_categories["periodic"]):
if not hasattr(self, f"_cutoff_n_{var_index}"):
self._make_property(
f"cutoff_n_{var_index}", 5, "update_cutoffs"
)
self.cutoff_names.append(f"cutoff_n_{var_index}")
if var_type == "extended":
for idx, var_index in enumerate(self.var_categories["extended"]):
if not hasattr(self, f"_cutoff_ext_{var_index}"):
self._make_property(
f"cutoff_ext_{var_index}", 30, "update_cutoffs"
)
self.cutoff_names.append(f"cutoff_ext_{var_index}")
self.dynamic_var_indices = (
self.var_categories["periodic"] + self.var_categories["extended"]
)
# default values for the parameters
for idx, param in enumerate(self.symbolic_params):
if not hasattr(self, param.name):
self._make_property(
param.name, self.symbolic_params[param], "update_param_vars"
)
# setting the ranges for flux ranges used for discrete phi vars
for var_index in self.var_categories["extended"]:
if var_index not in self.discretized_phi_range:
self.discretized_phi_range[var_index] = (-6 * np.pi, 6 * np.pi)
# external flux vars
for flux in self.external_fluxes:
# setting the default to zero external flux
if not hasattr(self, flux.name):
self._make_property(flux.name, 0.0, "update_external_flux_or_charge")
# offset and free charges
for charge_var in self.offset_charges + self.free_charges:
# default to zero offset charge
if not hasattr(self, charge_var.name):
self._make_property(
charge_var.name, 0.0, "update_external_flux_or_charge"
)
# changing the matrix type if necessary
if (
len((self.var_categories["extended"] + self.var_categories["periodic"]))
== 1
):
self.type_of_matrices = "dense"
self.hamiltonian_symbolic = self.symbolic_circuit.hamiltonian_symbolic
# if the flux is static, remove the linear terms from the potential
if not self.symbolic_circuit.use_dynamic_flux_grouping:
self.hamiltonian_symbolic = self._shift_harmonic_oscillator_potential(
self.hamiltonian_symbolic
)
self.potential_symbolic = self._shift_harmonic_oscillator_potential(
self.potential_symbolic.expand()
)
if system_hierarchy is not None:
self.hierarchical_diagonalization = (
system_hierarchy != [] and number_of_lists_in_list(system_hierarchy) > 0
)
if not self.hierarchical_diagonalization:
if self.is_purely_harmonic and not ext_basis:
self.normal_mode_freqs = self.symbolic_circuit.normal_mode_freqs
if self.ext_basis != "harmonic":
warnings.warn(
"Purely harmonic circuits need ext_basis to be set to 'harmonic'"
)
self.ext_basis = "harmonic"
self.generate_hamiltonian_sym_for_numerics()
self.ext_basis = ext_basis or self.ext_basis
if self.is_purely_harmonic and self.ext_basis == "harmonic":
# using the default methods
self.evals_method = None
self.evals_method_options = None
self._annihilation_operator_in_eigenbasis = None
self._diagonalize_purely_harmonic_hamiltonian()
else:
# list for updating necessary subsystems when calling build hilbertspace
self.affected_subsystem_indices = []
self.operators_by_name = None
self.system_hierarchy = system_hierarchy
if subsystem_trunc_dims is None:
raise Exception(
"The truncated dimensions attribute for hierarchical "
"diagonalization is not set."
)
self.subsystem_trunc_dims = subsystem_trunc_dims
self.generate_hamiltonian_sym_for_numerics()
self.ext_basis = ext_basis or self.ext_basis
self.generate_subsystems()
self.ext_basis = self.get_ext_basis()
self.update_interactions()
self._check_truncation_indices()
self.affected_subsystem_indices = list(range(len(self.subsystems)))
self._set_vars() # setting the attribute vars to store operator symbols
self.operators_by_name = self.set_operators()
# clear unnecessary attribs
self._clear_unnecessary_attribs()
if generate_noise_methods:
self.generate_noise_methods()
self._frozen = True
self.update()
[docs] def supported_noise_channels(self) -> List[str]:
"""Return a list of supported noise channels"""
if not hasattr(self, "_noise_methods_generated"):
raise Exception(
"Noise methods are not generated, please use configure() with generate_noise_methods=True to generate them."
)
return [
method_name
for method_name in self.__dict__
if "tphi_1_over_f" in method_name or "t1_" in method_name
]
[docs] def effective_noise_channels(self):
if not hasattr(self, "_noise_methods_generated"):
raise Exception(
"Noise methods are not generated, please use configure() with generate_noise_methods=True to generate them."
)
return [
method_name
for method_name in self.supported_noise_channels()
if not is_string_float(method_name[-1])
]
[docs] def sym_lagrangian(
self,
vars_type: str = "node",
print_latex: bool = False,
return_expr: bool = False,
) -> Union[sm.Expr, None]:
"""
Method that gives a user readable symbolic Lagrangian for the current instance
Parameters
----------
vars_type:
"node" or "new", fixes the kind of lagrangian requested, by default "node"
print_latex:
if set to True, the expression is additionally printed as LaTeX code
return_expr:
if set to True, all printing is suppressed and the function will silently
return the sympy expression
"""
if vars_type == "node":
lagrangian = self.lagrangian_node_vars
# replace v\theta with \theta_dot
for var_index in range(
1, 1 + len(self.symbolic_circuit.nodes) - self.is_grounded
):
lagrangian = lagrangian.replace(
sm.symbols(f"vφ{var_index}"),
sm.symbols("\\dot{φ_" + str(var_index) + "}"),
)
# break down the lagrangian into kinetic and potential part, and rejoin
# with evaluate=False to force the kinetic terms together and appear first
sym_lagrangian_PE_node_vars = self.potential_node_vars
for external_flux in self.external_fluxes:
sym_lagrangian_PE_node_vars = sym_lagrangian_PE_node_vars.replace(
external_flux,
sm.symbols(
"(2π"
+ "Φ_{"
+ str(get_trailing_number(str(external_flux)))
+ "})"
),
)
lagrangian = sm.Add(
(self._make_expr_human_readable(lagrangian + self.potential_node_vars)),
(self._make_expr_human_readable(-sym_lagrangian_PE_node_vars)),
evaluate=False,
)
elif vars_type == "new":
lagrangian = self.lagrangian_symbolic
# replace v\theta with \theta_dot
for var_index in self.dynamic_var_indices:
lagrangian = lagrangian.replace(
sm.symbols(f"vθ{var_index}"),
sm.symbols("\\dot{θ_" + str(var_index) + "}"),
)
# break down the lagrangian into kinetic and potential part, and rejoin
# with evaluate=False to force the kinetic terms together and appear first
sym_lagrangian_PE_new = self.potential_symbolic.expand()
for external_flux in self.external_fluxes:
sym_lagrangian_PE_new = sym_lagrangian_PE_new.replace(
external_flux,
sm.symbols(
"(2π"
+ "Φ_{"
+ str(get_trailing_number(str(external_flux)))
+ "})"
),
)
lagrangian = sm.Add(
(
self._make_expr_human_readable(
lagrangian + self.potential_symbolic.expand()
)
),
(self._make_expr_human_readable(-sym_lagrangian_PE_new)),
evaluate=False,
)
if return_expr:
return lagrangian
if print_latex:
print(latex(lagrangian))
if _HAS_IPYTHON:
self.print_expr_in_latex(lagrangian)
else:
print(lagrangian)
[docs] def sym_external_fluxes(self) -> Dict[sm.Expr, Tuple["Branch", List["Branch"]]]:
"""
Method returns a dictionary of Human readable external fluxes with associated
branches and loops (represented as lists of branches) for the current instance
Returns
-------
A dictionary of Human readable external fluxes with their associated
branches and loops
"""
return {
self._make_expr_human_readable(self.external_fluxes[ibranch]): (
self.closure_branches[ibranch],
self.symbolic_circuit._find_loop(self.closure_branches[ibranch]),
)
for ibranch in range(len(self.external_fluxes))
}
[docs] def oscillator_list(self, osc_index_list: List[int]):
"""
If hierarchical diagonalization is used, specify subsystems that corresponds to
single-mode oscillators, if there is any. The attributes `_osc_subsys_list` and
`osc_subsys_list` of the `hilbert_space` attribute of the Circuit instance will
be assigned accordingly, enabling the correct identification of harmonic modes
for the dispersive regime analysis in ParameterSweep.
Parameters
----------
osc_index_list:
a list of indices of subsystems that are single-mode harmonic oscillators
"""
# identify if each nominated subsystem indeed have a single harmonic oscillator
osc_subsys_list = []
for subsystem_index in osc_index_list:
subsystem = self.subsystems[subsystem_index]
if not subsystem.is_purely_harmonic:
raise Exception(
f"the subsystem {subsystem_index} is not purely harmonic"
)
elif len(subsystem.var_categories["extended"]) != 1:
raise Exception(
f"the subsystem has more than one harmonic oscillator mode"
)
else:
osc_subsys_list.append(subsystem)
self.hilbert_space._osc_subsys_list = osc_subsys_list
[docs] def qubit_list(self, qbt_index_list: List[int]):
"""
If hierarchical diagonalization is used, specify subsystems that corresponds to
single-mode oscillators, if there is any. The attributes `_osc_subsys_list` and
`osc_subsys_list` of the `hilbert_space` attribute of the Circuit instance will
be assigned accordingly, enabling the correct identification of harmonic modes
for the dispersive regime analysis in ParameterSweep.
Parameters
----------
qbt_index_list:
a list of indices of subsystems that are single-mode harmonic oscillators
"""
# identify if each naminated subsystem indeed have a single harmonic oscillator
qbt_subsys_list = []
for subsystem_index in qbt_index_list:
subsystem = self.subsystems[subsystem_index]
qbt_subsys_list.append(subsystem)
self.hilbert_space._qbt_subsys_list = qbt_subsys_list