__doc__ = "qpackage_interface.py: contains functions that interface with scqubits and sqcircuit"
__author__ = "Eli Weissler, Mohit Bhat"
__version__ = "0.1.0"
__all__ = ["to_SQcircuit", "to_SCqubits", "add_explicit_ground_node", "find_loops"]
# -------------------------------------------------------------------
# Import Statements
# -------------------------------------------------------------------
import numpy as np
import networkx as nx
import SQcircuit as sq
import scqubits as scq
from typing import Union
import sircuitenum.utils as utils
# -------------------------------------------------------------------
# Functions
# -------------------------------------------------------------------
def single_edge_loop_kiting(circuit, edges):
""" expands edges which contain loops by splitting inductors and
adding nodes. Done since networkx doesn't calcuate loops for multigraphs
Args:
circuit (list): a list of element labels for the desired circuit
e.g. [("J",),("L", "J"), ("C",)]
edges (list): a list of edge connections for the desired circuit
e.g. [(0,1), (0,2), (1,2)]
Returns:
copies of circuit/edges where every edge that contained both a J
and an L have been expanded into two L's with an intermediate node
"""
# Make copies
circuit_out = []
edges_out = []
# Highest index node present
max_node = utils.get_num_nodes(edges)-1
for element, edge in zip(circuit, edges):
# If we have a junction and an inductor, then there's a loop
# Make it understandable by loop finding
# By streching the inductor into two, adding an extra node
if "J" in element and "L" in element:
# Put in the edge without inductors
circuit_out.append(tuple([x for x in element if x != "L"]))
edges_out.append(edge)
# Make the inductors off to the side
circuit_out.append(("L",))
edges_out.append((edge[0], max_node + 1))
circuit_out.append(("L",))
edges_out.append((max_node + 1, edge[1]))
# Keep track of how many nodes you've added
max_node += 1
# If there's not both a junction and inductor
# then keep the edge the same
else:
circuit_out.append(element)
edges_out.append(edge)
return circuit_out, edges_out
[docs]def find_loops(circuit, edges, ind_elem=["J", "L"]):
"""
Generate a list of loops for a given circuit.
This function returns a list of loops for the specified circuit by identifying
the loops formed by the inductive elements and edge connections.
Parameters
----------
circuit : list
A list of element labels for the desired circuit.
Example: ``[("J",), ("L", "J"), ("C",)]``.
edges : list
A list of edge connections for the desired circuit.
Example: ``[(0, 1), (0, 2), (1, 2)]``.
ind_elem : list, optional
A list of symbols that define inductive elements. The default is
``["J", "L"]`` for Josephson junctions and inductors.
Returns
-------
tuple
A tuple containing:
- ``loop_lst`` (list): A list of loops in the circuit.
- ``circuit`` (list): The input circuit list.
- ``edges`` (list): The input edges list.
"""
# save min mode number for recovering afterwards
min_node = min(min(x) for x in edges)
# Renumber to start from 0
edges = utils.zero_start_edges(edges)
# Expand single edge loops
max_node_og = utils.get_num_nodes(edges)-1
circuit_temp, edges_temp = single_edge_loop_kiting(circuit, edges)
# Make a graph that represents only inductive edges
ind_edges = inductive_subgraph(circuit_temp, edges_temp, ind_elem)
G = nx.from_edgelist(ind_edges)
# Find loops in the inductive subgraph
# And filter out any edges that we added
loop_lst = [tuple(sorted([x for x in c if x <= max_node_og]))
for c in nx.cycle_basis(nx.Graph(G))]
# Add min node number to recover the original numbering
loop_lst = [tuple([n + min_node for n in nodes]) for nodes in loop_lst]
return loop_lst
def inductive_subgraph(circuit, edges, ind_elem=["J", "L"]):
"""Returns a list of edges that contain an inductive element
Args:
circuit (list): a list of element labels for the desired circuit
e.g. [["J"],["L", "J"], ["C"]]
edges (list): a list of edge connections for the desired circuit
e.g. [(0,1), (0,2), (1,2)]
ind_elem (list): symbols that define inductive elements.
Default is ind_elem = ["J", "L"]
"""
return [edges[i] for i in range(len(edges))
if np.any(np.in1d(circuit[i], ind_elem))]
[docs]def add_explicit_ground_node(circuit: list, edges: list, params: dict, ecg: float = 20,
rand_amp=0.0):
"""
Add an explicit ground node to the circuit and modify the edges.
This function takes a circuit and its edges and returns a modified version with
an explicit ground node (node 0) added. If node 0 was already present in the
edges, the function increments all node labels by 1.
Parameters
----------
circuit : list
A list of element labels for the desired circuit.
Example: ``[("J"), ("L", "J"), ("C")]``.
edges : list
A list of edge connections for the desired circuit.
Example: ``[(0, 1), (0, 2), (1, 2)]``.
params : dict
A dictionary containing parameter values for the circuit elements. The dictionary should
include entries for ``C``, ``L``, ``J``, and ``CJ``, which represent the values for
capacitors, inductors, Josephson junctions, and coupling capacitances, respectively.
Additionally, it may include units for these parameters as ``C_units``, ``L_units``,
``J_units``, and ``CJ_units``.
ecg : float
The EC (charging energy) for capacitors coupling to the ground, in GHz.
Returns
-------
tuple
A tuple containing the modified version of the circuit and edges with
capacitive coupling to a ground node added.
"""
# Get unique node values
edges_og = edges[:]
edges = utils.zero_start_edges(edges)
node_vals = []
for n1, n2 in edges:
if n1 not in node_vals:
node_vals.append(n1)
if n2 not in node_vals:
node_vals.append(n2)
node_vals = sorted(node_vals)
n_nodes = len(node_vals)
new_circuit = circuit + [("C",)]*n_nodes
new_edges = [(e[0] + 1, e[1] + 1) for e in edges] + [(0, n+1) for n in node_vals]
# Modify params for old elements
# to reflect new labeling
new_params = {}
n_edges_og = len(edges_og)
for i in range(n_edges_og):
edge_og = edges_og[i]
edge = new_edges[i]
for elem in circuit[i]:
new_params[(edge, elem)] = params[(edge_og, elem)]
if elem == "J" and (edge_og, "CJ") in params:
new_params[(edge, "CJ")] = params[(edge_og, "CJ")]
# Add the capacitive connections to ground
for i in range(n_edges_og, n_edges_og + n_nodes):
edge = new_edges[i]
elem = new_circuit[i][0]
new_params[(edge, elem)] = (ecg*np.random.normal(1, rand_amp), "GHz")
return new_circuit, new_edges, new_params
def swap_nodes(edges: list, na: int, nb: int):
new_edges = []
for (n0, n1) in edges:
# Swap na and nb
if n0 == nb:
n0 = na
elif n0 == na:
n0 = nb
if n1 == nb:
n1 = na
elif n1 == na:
n1 = nb
new_edges.append((n0, n1))
return new_edges
[docs]def to_SQcircuit(circuit: list, edges: list,
trunc_num: Union[int, list] = 50, **kwargs) -> sq.Circuit:
"""
Convert a circuit from a list of labels and edges to an SQcircuit-formatted circuit network.
This function converts the input circuit, specified by a list of element labels and edge
connections, into a circuit network compatible with the SQcircuit package.
Parameters
----------
circuit : list
A list of element labels for the desired circuit.
Example: ``[["J"], ["L", "J"], ["C"]]``.
edges : list
A list of edge connections for the desired circuit.
Example: ``[(0, 1), (0, 2), (1, 2)]``.
trunc_num : int or list
The truncation number for each mode in the circuit.
params : dict, optional
A dictionary containing the parameters for the circuit elements, including:
- ``C``, ``L``, ``J``, and ``CJ`` for the circuit components.
- Optional entries for units: ``C_units``, ``L_units``, ``J_units``, and ``CJ_units``.
If no parameters are provided, default values from ``utils.ELEM_DICT`` will be used.
Returns
-------
SQcircuit.Circuit
The input circuit, converted to the SQcircuit format.
"""
params = kwargs.get("params", utils.gen_param_dict(circuit, edges,
utils.ELEM_DICT,
rand_amp=kwargs.get("rand_amp", 0.00)))
flux_dist = kwargs.get("flux_dist", "junctions")
# ground node is node = 0
ground_node = kwargs.get("ground_node", None)
if ground_node is None:
circuit, edges, params = add_explicit_ground_node(circuit, edges, params)
elif ground_node != 0:
edges = swap_nodes(edges, 0, ground_node)
new_params = {}
for key in params:
edge, elem = key
new_edge = swap_nodes([edge], 0, ground_node)[0]
new_params[(new_edge, elem)] = params[(edge, elem)]
params = new_params
loops = find_loops(circuit, edges)
loop_defs = {}
# Map inductive cycle basis to loops
for lp in loops:
loop_defs[lp] = sq.Loop(id_str=str(lp))
# Build sqcircuit dictionary that maps edges
# to a list of element objects, which have
# their loops set
circuit_dict = {}
for elems, edge in zip(circuit, edges):
# Record all the loops for this edge
loops_pres_J = []
loops_pres_L = []
for lp in loops:
# If the edge is part of the loop
if edge[0] in lp and edge[1] in lp:
# Either J or L in the branch
if "J" in elems and "L" not in elems:
loops_pres_J.append(loop_defs[lp])
elif "J" not in elems and "L" in elems:
loops_pres_L.append(loop_defs[lp])
# Both J and L in the branch
else:
# two-node loop -- Flux between J & L
if len(lp)==2:
loops_pres_J.append(loop_defs[lp])
loops_pres_L.append(loop_defs[lp])
# > 2 node loop -- Assign flux to JJ
else:
loops_pres_J.append(loop_defs[lp])
# loops_pres_L.append(loop_defs[lp])
# print("edge:", edge)
# print('loops_pres J:', loops_pres_J)
# print("loops_pres L:",loops_pres_L)
# Add all the elements
circuit_dict[edge] = []
for elem in elems:
val, units = params[(edge, elem)]
units = "GHz"
if elem == "C":
id_str = "C_" + "".join([str(x) for x in edge])
circuit_dict[edge].append(sq.Capacitor(val, units,
id_str=id_str,
))
elif elem == "L":
id_str = "L_" + "".join([str(x) for x in edge])
circuit_dict[edge].append(sq.Inductor(val, units,
id_str=id_str,
loops=loops_pres_L))
elif elem == "J":
id_str = "J_" + "".join([str(x) for x in edge])
if (edge, "CJ") in params:
val2, units2 = params[(edge, "CJ")]
if val2 > 0:
j_c = sq.Capacitor(val2, units2, id_str="C"+id_str)
circuit_dict[edge].append(sq.Junction(val, units,
id_str=id_str,
loops=loops_pres_J,
cap=j_c))
else:
circuit_dict[edge].append(sq.Junction(val, units,
id_str=id_str,
loops=loops_pres_J)
)
else:
circuit_dict[edge].append(sq.Junction(val, units,
id_str=id_str,
loops=loops_pres_J))
else:
raise ValueError("Unknown circuit compenent present.\
Must be either C, J, or L")
sqC = sq.Circuit(circuit_dict, flux_dist=flux_dist)
# Convert truncation num to list
if isinstance(trunc_num, int):
trunc_num = [trunc_num]*sqC.n
if sqC.n > len(trunc_num):
# print("Warning, too few trunc nums given, filling in with max value (and adding 10)", max(trunc_num))
trunc_num = [x for x in trunc_num] + [np.max(trunc_num)]*(sqC.n-len(trunc_num))
elif sqC.n < len(trunc_num):
trunc_num = [np.max(trunc_num)]*sqC.n
sqC.set_trunc_nums([x for x in trunc_num])
return sqC
[docs]def to_SCqubits(circuit: list, edges: list,
trunc_num: Union[int, list] = 50,
cutoff: Union[int, list] = 101,
**kwargs) -> scq.Circuit:
"""
Convert a circuit from a list of labels and edges to an SCqubits-formatted circuit network.
This function converts the input circuit, specified by a list of element labels and edge
connections, into a circuit network compatible with the SCqubits package.
**Note:** This function only supports values in GHz.
Parameters
----------
circuit : list
A list of element labels for the desired circuit.
Example: ``[("J",), ("L", "J"), ("C",)]``.
edges : list
A list of edge connections for the desired circuit.
Example: ``[(0, 1), (0, 2), (1, 2)]``.
trunc_num : int or list
The number of eigenstates to consider for each mode in a composite circuit.
For more details, refer to: `SCqubits Custom Circuit Guide <https://scqubits.readthedocs.io/en/latest/guide/ipynb/custom_circuit_hd.html>`_.
cutoff : int or list
The number of points to use in the underlying position space for each mode.
params : dict, optional
A dictionary with entries for the circuit parameters, including:
- ``C``, ``L``, ``J``, and ``CJ`` for the circuit elements.
- Optional entries for units: ``C_units``, ``L_units``, ``J_units``, and ``CJ_units``.
If no parameters are provided, default values from ``utils.ELEM_DICT`` are used.
Returns
-------
scqubits.Circuit
The input circuit, converted to the SCqubits circuit format.
"""
params = kwargs.get("params", utils.gen_param_dict(circuit, edges,
utils.ELEM_DICT))
sym_cir = kwargs.get("sym_cir", False)
initiate_sym_calc = kwargs.get("initiate_sym_calc", True)
# ground node is node = 0
ground_node = kwargs.get("ground_node", None)
if ground_node is None:
edges = utils.zero_start_edges(edges)
edges = [(n1 + 1, n2 + 1) for (n1, n2) in edges]
new_params = {}
for key in params:
edge, elem = key
new_edge = (edge[0] + 1, edge[1] + 1)
new_params[(new_edge, elem)] = params[(edge, elem)]
params = new_params
elif ground_node != 0:
edges = swap_nodes(edges, 0, ground_node)
for key in params:
edge, elem = key
new_edge = swap_nodes([edge], 0, ground_node)[0]
new_params[(new_edge, elem)] = params[(edge, elem)]
params = new_params
basis_completion = kwargs.get("basis_completion", "heuristic")
# Build scqubits circuit yaml string
circuit_yaml = "branches:"
for elems, edge in zip(circuit, edges):
edge_str = "_".join([str(x) for x in edge])
# Add all the elements
for elem in elems:
val = f"{elem}_{edge_str} = "
if elem == "J":
e_str = "JJ"
val1, _ = params[(edge), elem]
if (edge, "CJ") in params:
val2, _ = params[(edge), "CJ"]
if val2 > 0:
val += f"{val1}, {val2}"
else:
val += f"{val1}, 1000.0"
else:
val += f"{val1}, 1000.0"
else:
e_str = elem
val += f"{params[(edge), elem][0]}"
circuit_yaml += "\n"
circuit_yaml += f'- ["{e_str}", {edge[0]}, {edge[1]}, {val}]'
if sym_cir:
return scq.SymbolicCircuit.from_yaml(circuit_yaml, from_file=False,
basis_completion=basis_completion,
initiate_sym_calc=initiate_sym_calc)
else:
conv = scq.Circuit(circuit_yaml, from_file=False,
basis_completion=basis_completion,
generate_noise_methods=True)
# Set cutoff and count number of modes
n_nodes = utils.get_num_nodes(edges)
n_modes = 0
if not isinstance(cutoff, list):
if n_nodes > 2:
cutoff = [cutoff]*(n_nodes - 1)
else:
cutoff = [cutoff]
for mode_type in ['periodic', 'extended']:
if mode_type == "periodic":
mode_str = "n"
elif mode_type == "extended":
mode_str = "ext"
for mode in conv.var_categories[mode_type]:
n_modes += 1
exec(f"conv.cutoff_{mode_str}_{mode}={cutoff[mode-1]}")
# Set truncation
if n_modes > 1:
hier = [[x] for x in np.arange(n_modes) + 1]
if not isinstance(trunc_num, list):
trunc_num = [trunc_num]*(n_modes)
conv.configure(system_hierarchy=hier,
subsystem_trunc_dims=trunc_num)
return conv