Source code for SQcircuit.elements

"""elements.py contains the classes for the circuit elements:
capacitors, inductors, and josephson junctions.
"""

from typing import List, Any, Optional, Union, Callable

import torch
from torch.autograd.function import once_differentiable
import numpy as np

from scipy.special import kn, kvp
from torch import Tensor
from numpy import ndarray

import SQcircuit.units as unt
import SQcircuit.functions as sqf

from SQcircuit.logs import (
    raise_unit_error,
    raise_optim_error_if_needed,
)
from SQcircuit.settings import get_optim_mode

###############################################################################
# Special functions
###############################################################################


class KnSolver(torch.autograd.Function):
    @staticmethod
    def forward(ctx, n: int, x):
        ctx.save_for_backward(x)
        ctx.order = n
        x = sqf.numpy(x)
        return torch.as_tensor(kn(n, x))

    @staticmethod
    @once_differentiable
    def backward(ctx, grad_output):
        z, = ctx.saved_tensors
        return None, grad_output * kvp(ctx.order, z)

###############################################################################
# Elements
###############################################################################


class Element:
    """Class that contains general properties of elements."""

    _error = None
    _value = None

    @property
    def internal_value(self) -> Union[float, Tensor]:
        return self._value

    @internal_value.setter
    def internal_value(self, v: Union[float, Tensor]) -> None:
        self._value = v

    @property
    def error(self) -> float:
        return self._error

    @error.setter
    def error(self, e: float) -> None:
        self._error = e

    @property
    def requires_grad(self) -> bool:
        raise_optim_error_if_needed()

        return self.internal_value.requires_grad

    @property
    def is_leaf(self) -> bool:
        raise_optim_error_if_needed()

        return self.internal_value.is_leaf

    @requires_grad.setter
    def requires_grad(self, f: bool) -> None:

        raise_optim_error_if_needed()

        self._value.requires_grad = f

    def _set_value_with_error(self, mean: float, error: float) -> None:
        mean_th = torch.as_tensor(mean, dtype=torch.float64)
        error_th = torch.as_tensor(error, dtype=torch.float64)

        sampled_value = torch.normal(mean_th, mean_th*error_th/100)
        self._value = sampled_value

        if not get_optim_mode():
            self._value = float(self._value.detach().cpu().numpy())

    def get_value(self) -> Union[float, Tensor]:
        raise NotImplementedError

    @staticmethod
    def _get_default_id_str(s: str, v: float, u: str) -> str:
        """Get the default string ID for the element.
        
        Parameters
        ----------
            s:
                The initial string of the id_string.
            v:
                The value of the element as float number.
            u:
                The unit of the element as string.
        """
        assert isinstance(s, str), "The input must have string format."

        return (s + "_{}_{}").format(v, u)


[docs] class Capacitor(Element): """Class that contains the capacitor properties. Parameters ---------- value: The value of the capacitor. unit: The unit of input value. If ``unit`` is "THz", "GHz", and, etc., the value specifies the charging energy of the capacitor. If ``unit`` is "fF", "pF", and, etc., the value specifies the capacitance in farad. If ``unit`` is ``None``, the default unit of capacitor is "GHz". requires_grad: A boolean variable that specifies whether autograd should record operations on this element. This feature is specific to the ``PyTorch`` engine. Q: Quality factor of the dielectric of the capacitor which is one over tangent loss. It can be either a float number or a Python function of angular frequency. error: The error of fabrication in percentage. id_str: ID string for the capacitor. """ value_unit = "F" def __init__( self, value: float, unit: Optional[str] = None, requires_grad: bool = False, Q: Union[Any, Callable[[float], float]] = "default", error: float = 0, id_str: Optional[str] = None, ) -> None: if unit is None: unit = unt.get_unit_cap() else: self._check_unit_format(unit) self.set_value(value, unit, error) self.type = type(self) if requires_grad: self.requires_grad = requires_grad if Q == "default": self.Q = self._default_q_cap elif isinstance(Q, float) or isinstance(Q, int): self.Q = lambda omega: Q else: self.Q = Q if id_str is None: self.id_str = self._get_default_id_str("C", value, unit) else: self.id_str = id_str @staticmethod def _check_unit_format(u: str): """Check if the unit input has the correct format.""" if (u not in unt.freq_list and u not in unt.farad_list and u is not None): raise_unit_error()
[docs] def set_value(self, v: float, u: str = 'F', e: float = 0.0) -> None: """Set the value for the capacitor. Parameters ---------- v: The value of the element. u: The unit of input value. e: The fabrication error in percentage. """ self._check_unit_format(u) self.error = e if u in unt.farad_list: mean = v * unt.farad_list[u] else: energy = v * unt.freq_list[u] * (2*np.pi*unt.hbar) mean = unt.e ** 2 / 2 / energy self._set_value_with_error(mean, e)
[docs] def get_value(self, u: str = "F") -> Union[float, Tensor]: """Return the value of the element in specified unit. Parameters ---------- u: The unit of input value. The default is "F". """ if u in unt.farad_list: return self._value / unt.farad_list[u] elif u in unt.freq_list: E_c = unt.e**2/2/self._value/(2*np.pi*unt.hbar)/unt.freq_list[u] return E_c else: raise_unit_error()
@staticmethod def partial_mat(edge_mat): return edge_mat @staticmethod def _default_q_cap(omega): """Default function for capacitor quality factor.""" return 1e6 * (2 * np.pi * 6e9 / np.abs(sqf.numpy(omega)))**0.7
class VerySmallCap(Capacitor): def __init__(self): super().__init__(1e-20, "F", Q=None) class VeryLargeCap(Capacitor): def __init__(self): super().__init__(1e20, "F", Q=None)
[docs] class Inductor(Element): """Class that contains the inductor properties. Parameters ---------- value: The value of the inductor. unit: The unit of input value. If ``unit`` is "THz", "GHz", and ,etc., the value specifies the inductive energy of the inductor. If ``unit`` is "fH", "pH", and ,etc., the value specifies the inductance in henry. If ``unit`` is ``None``, the default unit of inductor is "GHz". requires_grad: A boolean variable that specifies whether autograd should record operations on this element. This feature is specific to the ``PyTorch`` engine. loops: List of loops in which the inductor resides. cap: Capacitor associated to the inductor, necessary for correct time-dependent external fluxes scheme. Q: Quality factor of the inductor needed for inductive loss calculation. It can be either a float number or a Python function of angular frequency and temperature. error: The error in fabrication as a percentage. id_str: ID string for the inductor. """ value_unit = "H" def __init__( self, value: float, unit: str = None, requires_grad: bool = False, cap: Optional["Capacitor"] = None, Q: Union[Any, Callable[[float, float], float]] = "default", error: float = 0, loops: Optional[List["Loop"]] = None, id_str: Optional[str] = None ) -> None: if unit is None: unit = unt.get_unit_ind() else: self._check_unit_format(unit) self.set_value(value, unit, error) self.type = type(self) if requires_grad: self.requires_grad = requires_grad if cap is None: self.cap = VerySmallCap() else: self.cap = cap if loops is None: self.loops = [] else: self.loops = loops if Q == "default": self.Q = self._default_q_ind elif isinstance(Q, float) or isinstance(Q, int): self.Q = lambda omega, T: Q else: self.Q = Q if id_str is None: self.id_str = self._get_default_id_str("L", value, unit) else: self.id_str = id_str @staticmethod def _check_unit_format(u): """Check if the unit input has the correct format.""" if ( u not in unt.freq_list and u not in unt.henry_list and u is not None ): raise_unit_error()
[docs] def set_value(self, v: float, u: str = 'H', e: float = 0.0) -> None: """Set the value for the element. Parameters ---------- v: The value of the element. u: The unit of input value. e: The fabrication error in percentage. """ self._check_unit_format(u) self.error = e if u in unt.henry_list: mean = v * unt.henry_list[u] else: energy = v * unt.freq_list[u] * (2*np.pi*unt.hbar) mean = (unt.Phi0/2/np.pi)**2 / energy self._set_value_with_error(mean, e)
[docs] def get_value(self, u: str = "H") -> Union[float, Tensor]: """Return the value of the element in specified unit. Parameters ---------- u: The unit of input value. The default is "H". """ if u in unt.henry_list: return self._value / unt.henry_list[u] elif u in unt.freq_list: l = self._value E_l = (unt.Phi0/2/np.pi)**2/l/(2*np.pi*unt.hbar)/unt.freq_list[u] return E_l else: raise_unit_error()
@staticmethod def _default_q_ind(omega, T): """Default function for inductor quality factor.""" alpha = unt.hbar * 2 * np.pi * 0.5e9 / (2 * unt.k_B * T) beta = unt.hbar * sqf.numpy(omega) / (2 * unt.k_B * T) return 500e6*(kn(0, alpha)*np.sinh(alpha))/(kn(0, beta)*np.sinh(beta)) def get_key(self, edge, B_idx, *_): """Return the inductor key. Parameters ---------- edge: Edge that element is part of. B_idx: The inductive element index """ return edge, self, B_idx def get_cap_for_flux_dist(self, flux_dist): if flux_dist == 'all': return self.cap.get_value() elif flux_dist == "junctions": return VeryLargeCap().get_value() elif flux_dist == "inductors": return VerySmallCap().get_value() def partial_mat(self, edge_mat: ndarray) -> ndarray: """Get the partial_mat based on input edge_mat. Parameters ---------- edge_mat: Matrix representation of the edge that element is part of. """ return edge_mat / sqf.numpy(self.get_value()**2)
[docs] class Junction(Element): """Class that contains the Josephson junction properties. Parameters ----------- value: The value of the Josephson junction. unit: str The unit of input value. The ``unit`` can be "THz", "GHz", and ,etc., that specifies the junction energy of the inductor. If ``unit`` is ``None``, the default unit of junction is "GHz". requires_grad: A boolean variable that specifies whether autograd should record operations on this element. This feature is specific to the ``PyTorch`` engine. loops: List of loops in which the Josephson junction reside. cap: Capacitor associated to the josephson junction, necessary for the correct time-dependent external fluxes scheme. A: Normalized noise amplitude related to critical current noise. x: Quasiparticle density delta: Superconducting gap Y: Real part of admittance. error: The error in fabrication as a percentage. id_str: ID string for the junction. """ value_unit = "Hz" def __init__( self, value: float, unit: Optional[str] = None, requires_grad: bool = False, cap: Optional[Capacitor] = None, A: float = 1e-7, x: float = 3e-06, delta: float = 3.4e-4, Y: Union[Any, Callable[[float, float], float]] = "default", error: float = 0, loops: Optional[List["Loop"]] = None, id_str: Optional[str] = None, ) -> None: if unit is None: unit = unt.get_unit_JJ() else: self._check_unit_format(unit) self.set_value(value, unit, error) self.type = type(self) self.A = A if requires_grad: self.requires_grad = requires_grad if cap is None: self.cap = VerySmallCap() else: self.cap = cap if loops is None: self.loops = [] else: self.loops = loops if Y == "default": self.Y = self.__get_default_y_func(delta, x) else: self.Y = Y if id_str is None: self.id_str = self._get_default_id_str("JJ", value, unit) else: self.id_str = id_str @staticmethod def _check_unit_format(u): """Check if the unit input has the correct format.""" if u not in unt.freq_list and u is not None: raise_unit_error()
[docs] def set_value(self, v: float, u: str = 'Hz', e: float = 0.0) -> None: """Set the value for the element. Parameters ---------- v: The value of the element. u: The unit of input value. e: The fabrication error in percentage. """ self._check_unit_format(u) self.error = e mean = v * unt.freq_list[u] * 2 * np.pi self._set_value_with_error(mean, e)
[docs] def get_value(self, u: str = "Hz") -> Union[float, Tensor]: """Return the value of the element in specified unit. Parameters ---------- u: The unit of input value. The default is "Hz". """ if u in unt.freq_list: return self._value / unt.freq_list[u] else: raise_unit_error()
def get_key(self, edge, B_idx, W_idx, *_): """Return the junction key. Parameters ---------- edge: Edge that element is part of. B_idx: The inductive element index W_idx: The JJ index """ return edge, self, B_idx, W_idx def get_cap_for_flux_dist(self, flux_dist): if flux_dist == 'all': return self.cap.get_value() elif flux_dist == "junctions": return VerySmallCap().get_value() elif flux_dist == "inductors": return VeryLargeCap().get_value() @staticmethod def __get_default_y_func( delta: float, x: float ) -> Callable[[Union[float, Tensor]], float]: def _default_y_junc( omega: Union[float, Tensor], T: float ) -> Union[float, Tensor]: """Default function for junction admittance.""" alpha = unt.hbar * omega / (2 * unt.k_B * T) y = np.sqrt(2 / np.pi) * (8 / (delta * 1.6e-19) / ( unt.hbar * 2 * np.pi / unt.e ** 2)) \ * (2 * (delta * 1.6e-19) / unt.hbar / omega) ** 1.5 \ * x * sqf.sqrt(alpha) * KnSolver.apply(0, alpha) * sqf.sinh(alpha) return y return _default_y_junc
[docs] class Loop: """Class that contains the inductive loop properties, closed path of inductive elements. Parameters ---------- value: Value of the external flux in the loop. requires_grad: A boolean variable that specifies whether autograd should record operations on this loop. This feature is specific to the ``PyTorch`` engine. A: Normalized noise amplitude related to flux noise. id_str: ID string for the loop. """ def __init__( self, value: float = 0, A: float = 1e-6, requires_grad: bool = False, id_str: Optional[str] = None ) -> None: self.set_flux(value) if requires_grad: self.requires_grad = requires_grad self.A = A * 2 * np.pi # indices of inductive elements. self.indices = [] # k1 matrix related to this specific loop self.K1 = [] if id_str is None: self.id_str = "loop" else: self.id_str = id_str @property def requires_grad(self) -> bool: raise_optim_error_if_needed() return self.lpValue.requires_grad @requires_grad.setter def requires_grad(self, f: bool) -> None: raise_optim_error_if_needed() self.lpValue.requires_grad = f @property def is_leaf(self) -> bool: raise_optim_error_if_needed() return self.internal_value.is_leaf def reset(self) -> None: self.K1 = [] self.indices = []
[docs] def value(self, random: bool = False) -> float: """Return the value of the external flux. If `random` is `True`, it samples from a normal distribution with variance defined by the flux noise amplitude. Parameters ---------- random: A boolean flag which specifies whether the output is deterministic or random. """ if not random: return self.lpValue else: return np.random.normal(self.lpValue, self.A, 1)[0]
[docs] def set_flux(self, value: float) -> None: """Set the external flux associated to the loop. Parameters ---------- value: The external flux value """ if get_optim_mode(): value = torch.as_tensor(value) self.lpValue = value * 2 * np.pi
def add_index(self, index): self.indices.append(index) def addK1(self, w): self.K1.append(w) def getP(self): K1 = np.array(self.K1) a = np.zeros_like(K1) select = np.sum(K1 != a, axis=0) != 0 # eliminate the zero columns K1 = K1[:, select] if K1.shape[0] == K1.shape[1]: K1 = K1[:, 0:-1] b = np.zeros((1, K1.shape[0])) b[0, 0] = 1 p = np.linalg.inv(np.concatenate((b, K1.T), axis=0)) @ b.T return p.T @property def internal_value(self) -> Union[float, Tensor]: return self.lpValue @internal_value.setter def internal_value(self, v: Union[float, Tensor]) -> None: self.lpValue = v
class Charge: """Class that contains the charge island properties. """ def __init__(self, value: float = 0, A: float = 1e-4) -> None: """ inputs: -- value: The value of the offset. -- noise: The amplitude of the charge noise. """ self.chValue = value self.A = A def value(self, random: bool = False) -> float: """Returns the value of charge bias. If ``random`` flag is true, it samples from a normal distribution. inputs: -- random: A flag which specifies whether the output is picked deterministically or randomly. """ if not random: return self.chValue else: return np.random.normal(self.chValue, self.noise, 1)[0] def setOffset(self, value: float) -> None: self.chValue = value def setNoise(self, A: float) -> None: self.A = A