def __init__(self, name: str, probs: typing.List[float], level: int): """ Parameters ---------- name: str what the name of the noise is. Determines how many probabilites are needed additionally. probs: list: a list of probabilities with which to apply the requested noise level: int: the number of qubits in the gates this noise acts upon. """ probs = list_assignment(probs) if name not in noises_available: raise TequilaException( 'The name you asked for, {}, is not recognized'.format(name)) self._name = name self._level = int(level) if len(probs) != self.prob_length[name]: raise TequilaException( '{} noise requires {} probabilities; recieved {}'.format( name, self.prob_length[name], len(probs))) if name in krausses: assert sum(probs) <= 1. self.probs = list_assignment(probs)
def __init__(self, name, target: UnionList, control: UnionList = None, generator: QubitHamiltonian = None): self._name = name self._target = tuple(list_assignment(target)) self._control = tuple(list_assignment(control)) self.finalize() self.generator = generator
def __init__(self, name, target: UnionList, control: UnionList = None): self._name = name self._target = tuple(list_assignment(target)) self._control = tuple(list_assignment(control)) self.finalize() # Set the active qubits if self.control: self._qubits = self.target + self.control else: self._qubits = self.target self._qubits = sorted(tuple(set(self._qubits))) self._max_qubit = self.compute_max_qubit()
def AmplitudeDamp(p: float, level: int): ''' Returns a NoiseModel one QuantumNoise, corresponding to amplitude damping. this channel takes 1 to 0, but leaves 0 unaffected. kraus maps: E_0= [[1,0], [0,sqrt(1-p)]] E_1= [[0,sqrt(p)], [0,0]] Parameters ---------- p: float: the probability with which the noise is applied. level: int: the # of qubits in operations to apply this noise to. Returns ------- NoiseModel ''' new = NoiseModel.wrap_noise( QuantumNoise(name='amplitude damp', probs=list_assignment(p), level=level)) return new
def __init__(self, generators: typing.Union[QubitHamiltonian, typing.List[QubitHamiltonian]], steps: int = 1, angles: typing.Union[list, numbers.Real, Variable] = None, control: typing.Union[list, int] = None, threshold: numbers.Real = 0.0, join_components: bool = True, randomize_component_order: bool = True, randomize: bool = True): """ :param generators: list of generators :param angles: coefficients for each generator :param steps: Trotter Steps :param control: control qubits :param threshold: neglect terms in the given Hamiltonians if their coefficients are below this threshold :param join_components: The generators are trotterized together. If False the first generator is trotterized, then the second etc Note that for steps==1 as well as len(generators)==1 this has no effect :param randomize_component_order: randomize the order in the generators order before trotterizing :param randomize: randomize the trotter decomposition of each generator """ super().__init__(name="Trotterized", target=self.extract_targets(generators), control=control) self.generators = list_assignment(generators) self.angles = angles self.steps = steps self.threshold = threshold self.join_components = join_components self.randomize_component_order = randomize_component_order self.randomize = randomize self.finalize()
def _initialize_power_gate(name: str, target: typing.Union[list, int], generator, control: typing.Union[list, int] = None, power=None, angle=None) -> QCircuit: target = list_assignment(target) if angle is not None: angle = assign_variable(angle) if power is not None: power = power * angle / np.pi else: power = angle / np.pi if power is None or power in [1, 1.0]: gates = [ impl.QGateImpl(name=name, target=q, control=control, generator=generator(q)) for q in target ] else: gates = [ impl.PowerGateImpl(name=name, power=power, target=q, control=control, generator=generator(q)) for q in target ] return QCircuit.wrap_gate(gates)
def __init__(self, noises: typing.List[typing.Union[dict, QuantumNoise]] = None): if noises is None: self.noises = [] else: self.noises = [ QuantumNoise.from_dict(d) for d in list_assignment(noises) ]
def Z(qubit) -> QubitHamiltonian: """ Initialize a single Pauli Z Operator Parameters ---------- qubit: int or list of ints qubit(s) on which the operator should act Returns ------- QubitHamiltonian """ qubit = list_assignment(qubit) return pauli(qubit=qubit, type=["Z"] * len(qubit))
def Phase(target: typing.Union[list, int], control: typing.Union[list, int] = None, angle: typing.Union[typing.Hashable, numbers.Number] = None, *args, **kwargs) -> QCircuit: """ Notes ---------- Initialize an abstract phase gate which acts as .. math:: S(\\phi) = \\begin{pmatrix} 1 & 0 \\\\ 0 & e^{i\\phi} \\end{pmatrix} Parameters ---------- angle defines the phase, can be numeric type (static gate) or hashable non-numeric type (parametrized gate) target int or list of int control int or list of int Returns ------- QCircuit object """ # ensure backward compatibility if "phi" in kwargs: if angle is None: angle = kwargs["phi"] else: raise Exception( "tq.gates.Phase initialization: You gave two angles angle={} and phi={}. Please only use angle" .format(angle, kwargs["phi"])) if angle is None: angle = np.pi target = list_assignment(target) gates = [ impl.PhaseGateImpl(phase=angle, target=q, control=control) for q in target ] return QCircuit.wrap_gate(gates)
def PhaseFlip(p: float, level: int): ''' Returns a NoiseModel of one QuantumNoise, having a kraus map corresponding to applying pauli Z with likelihood p. Parameters ---------- p: float: the probability with which the noise is applied. level: int: the # of qubits in operations to apply this noise to. Returns ------- NoiseModel ''' new = NoiseModel.wrap_noise( QuantumNoise(name='phase flip', probs=list_assignment(p), level=level)) return new
def RotationGate(axis: int, angle: typing.Union[typing.Hashable, numbers.Number], target: typing.Union[list, int], control: typing.Union[list, int] = None, assume_real=False): """ Notes ---------- Initialize an abstract rotation gate of the form .. math:: R_{\\text{axis}}(\\text{angle}) = e^{-i\\frac{\\text{angle}}{2} \\sigma_{\\text{axis}}} Parameters ---------- axis integer 1 for x, 2 for y, 3 for z angle Hashable type (will be treated as Variable) or Numeric type (static angle) target integer or list of integers control integer or list of integers assume_real enable improved gradient compilation for controlled gates (wavefunction needs to be real) Returns ------- QCircuit object with this RotationGate """ target = list_assignment(target) gates = [ impl.RotationGateImpl(axis=axis, angle=angle, target=q, control=control, assume_real=assume_real) for q in target ] return QCircuit.wrap_gate(gates)
def DepolarizingError(p: float, level: int): ''' Returns a NoiseModel with one QuantumNoise, having a kraus map corresponding to equal probabilities of each of the three pauli matrices being applied. Parameters ---------- p: float: the probability with which the noise is applied. level: int: the # of qubits in operations to apply this noise to. Returns ------- NoiseModel ''' new = NoiseModel.wrap_noise( QuantumNoise(name='depolarizing', probs=list_assignment(p), level=level)) return new
def PhaseAmplitudeDamp(p1: float, p2: float, level: int): ''' Returns a NoiseModel with one QuantumNoise, having a kraus map corresponding to phase and amplitude damping. Parameters ---------- p1: float: the probability with which phase is damped p2: float: the probability with which amplitude is damped level: int: the # of qubits in operations to apply this noise to. Returns ------- NoiseModel ''' new = NoiseModel.wrap_noise( QuantumNoise(name='phase-amplitude damp', probs=list_assignment([p1, p2]), level=level)) return new
def _initialize_power_gate(name: str, target: typing.Union[list, int], generator, control: typing.Union[list, int] = None, power=None, angle=None, *args, **kwargs) -> QCircuit: target = list_assignment(target) # allow angle instead of power in initialization for more consistency # if angle is given we just convert it if angle is not None: angle = assign_variable(angle) if power is not None: power = power * angle / np.pi else: power = angle / np.pi if power is None or power in [1, 1.0]: gates = [ impl.QGateImpl(name=name, target=q, control=control, generator=generator(q)) for q in target ] else: gates = [ impl.PowerGateImpl(name=name, power=power, target=q, control=control, generator=generator(q), *args, **kwargs) for q in target ] return QCircuit.wrap_gate(gates)
def PhaseDamp(p: float, level: int): ''' Returns a NoiseModel of one QuantumNoise, having a kraus map corresponding to phase damping; Krauss map is defined following Nielsen and Chuang; E_0= [[1,0], [0,sqrt(1-p)]] E_1= [[0,0], [0,sqrt(p)]] Parameters ---------- p: float: the probability with which the noise is applied. level: int: the # of qubits in operations to apply this noise to. Returns ------- NoiseModel ''' new = NoiseModel.wrap_noise( QuantumNoise(name='phase damp', probs=list_assignment(p), level=level)) return new
def Sm(qubit) -> QubitHamiltonian: """ Notes ---------- Initialize .. math:: \\frac{1}{2} \\left( \\sigma_x + i \\sigma_y \\right) Parameters ---------- qubit: int or list of ints qubit(s) on which the operator should act Returns ------- QubitHamiltonian """ qubit = list_assignment(qubit) result = I() for q in qubit: result *= 0.5 * (X(qubit=q) - 1.j * Y(qubit=q)) return result
def get_torch_function(objective: Objective, compile_args: dict = None, input_vars: list = None): """ build a torch autograd function that calls the Objective; return it, and other useful objects. Parameters ---------- objective: Objective: the Objective to be transformed into a torch layer. compile_args: dict: a dictionary of arguments for the tequila compiler; used to render objectives callable input_vars: list: a list of variables; indicates which variables' values will be considered input to the layer. Returns ------- tuple: the requisite pytorch autograd function, alongside necessary information for higher level classes. """ if isinstance(objective, tuple) or isinstance( objective, list) or isinstance(objective, Objective): objective = vectorize(list_assignment(objective)) comped_objective, compile_args, input_vars, weight_vars, i_grads, w_grads, first, second \ = preamble(objective, compile_args, input_vars) samples = compile_args['samples'] def tensor_fix(tensor, angles, first, second): """ take a pytorch tensor and a dict of int,Variable to create a variable,float dictionary therefrom. Parameters ---------- tensor: torch.Tensor: a tensor. angles: torch.Tensor: first: dict: dict of int,Variable pairs indicating which position in Tensor corresponds to which variable. second: dict: dict of int,Variable pairs indicating which position in angles corresponds to which variable. Returns ------- dict: dict of variable, float pairs. Can be used as call arg by underlying tq objectives """ back = {} if tensor is not None: for i, val in enumerate(tensor): back[first[i]] = val.item() if angles is not None: for i, val in enumerate(angles): back[second[i]] = val.item() return back class _TorchFunction(torch.autograd.Function): """ Internal class for the forward and backward passes of calling a tequila objective. Notes ----- Though this class is a private class, some explanations of it's implementation may benefit curious users and later developers. Question: why is this class defined within a function? answer: because, since its defined entirely with staticmethods -- about which torch is quite particular -- it is impossible to use the attribute 'self' to store information for later use. This means that, to wrap arbitrary tequila Objectives into this class, the class needs to see the in some kind of well contained scope; the function containing this class, get_torch_function, provides that scope. in particular, this scoping is used to associate, with this function, arbitrary tequila objectives, their gradients with respect to weights or inputs (that is, variables specified to be one or the other) and a small dictionary called pattern, which orders the tequila variables w.r.t the order in which a tensor of combined input values and weight values are passed down to the function. Though this class doesn't have any proper attributes seperate from those it inherits, we detail the non-torch objects called within the function here: For Forward comped_objective: Objective a compiled tequila objective; this function has merely wrapped around it to pass torch Tensors into it. For Forward and Backward samples: int or None: how many samples the user wants when sampling the Objective or it's gradients. methods called: tensor_fix: takes a tensor and an (int: Variable) dict and returns a (Variable: float) dict. """ @staticmethod def forward(ctx, inputs, angles): """ forward pass of the function. """ ctx.save_for_backward(inputs, angles) call_args = tensor_fix(inputs, angles, first, second) result = comped_objective(variables=call_args, samples=samples) print(result) if not isinstance(result, np.ndarray): # this happens if the Objective is a scalar since that's usually more convenient for pure quantum stuff. result = np.array(result) if hasattr(inputs, 'device'): if inputs.device == 'cuda': r = torch.from_numpy(result).to(inputs.device) else: r = torch.from_numpy(result) else: r = torch.from_numpy(result) r.requires_grad_(True) return r @staticmethod def backward(ctx, grad_backward): inputs, angles = ctx.saved_tensors call_args = tensor_fix(inputs, angles, first, second) back_d = grad_backward.get_device() # build up weight and input gradient matrices... see what needs to be done to them. grad_outs = [None, None] for i, grads in enumerate([i_grads, w_grads]): if grads != {}: g_keys = [j for j in grads.keys()] probe = grads[ g_keys[0]] # first entry will tell us number of output dims = len(g_keys), len(probe) arr = np.empty(dims, dtype=np.float) for j, key in enumerate(g_keys): line = grads[key] for k, ob in enumerate(line): arr[j, k] = ob(variables=call_args, samples=samples) if back_d >= 0: g_tensor = torch.as_tensor(arr, dtype=grad_backward.dtype, device=back_d) else: g_tensor = torch.as_tensor(arr, dtype=grad_backward.dtype) b = grad_backward.reshape(-1, 1) jvp = torch.matmul(g_tensor, b) jvp_out = jvp.flatten() jvp_out.requires_grad_(True) grad_outs[i] = jvp_out return tuple(grad_outs) return _TorchFunction, weight_vars, compile_args
def __init__(self, objective: Union[Objective, VectorObjective], compile_args: Dict[str, Any] = None, input_vars: Dict[str, Any] = None, **kwargs): """ Tensorflow layer that compiles the Objective (or VectorObjective) with the given compile arguments and/or input variables if there are any when initialized. When called, it will forward the input variables into the compiled objective (if there are any inputs needed) alongside the parameters and will return the output. The gradient values can also be returned. Parameters ---------- objective Objective or VectorObjective to compile and run. compile_args dict of all the necessary information to compile the objective input_vars List of variables that will be inputs """ super(TFLayer, self).__init__(**kwargs) # Currently, the optimizers in tf.keras.optimizers don't support float64. For now, all values will be cast to # float32 to accommodate this, but in the future, whenever it is supported, this can be changed with # set_cast_type() self._cast_type = tf.float32 self.objective = objective # Store the objective and vectorize it if necessary if isinstance(objective, tuple) or isinstance(objective, list): for i, elem in enumerate(objective): if not isinstance(elem, Objective): raise TequilaMLException( "Element {} in {} is not a Tequila Objective: {}" "".format(i, type(objective), elem)) objective = vectorize(list_assignment(objective)) elif isinstance(objective, Objective) or isinstance( objective, VectorObjective): objective = vectorize(list_assignment(objective)) else: raise TequilaMLException( "Objective must be a Tequila Objective, VectorObjective " "or list/tuple of Objectives. Received a {}".format( type(objective))) self.objective = objective # Compile the objective, prepare the gradients and whatever else that may be necessary self.comped_objective, self.compile_args, self.input_vars, self.weight_vars, self.i_grads, self.w_grads, \ self.first, self.second = preamble(objective, compile_args, input_vars) # VARIABLES # These variables will hold 1D tensors which each will store the values in the order found by self.input_vars # for the variable in self.input_variable, and in the order found by self.weight_vars for the variable in # self.weight_variable # If there are inputs, prepare an input tensor as a trainable variable # NOTE: if the user specifies values for the inputs, they will be assigned in the set_input_values() if self.input_vars: initializer = tf.constant_initializer( np.random.uniform(low=0., high=2 * np.pi, size=len(self.input_vars))) self.input_variable = self.add_weight(name="input_tensor_variable", shape=(len(self.input_vars)), dtype=self._cast_type, initializer=initializer, trainable=True) else: self.input_variable = None # If there are weight variables, prepare a params tensor as a trainable variable if self.weight_vars: # Initialize the variable tensor that will hold the weights/parameters/angles initializer = tf.constant_initializer( np.random.uniform(low=0., high=2 * np.pi, size=len(self.weight_vars))) self.weight_variable = self.add_weight( name="params_tensor_variable", shape=(len(self.weight_vars)), dtype=self._cast_type, initializer=initializer, trainable=True) # If the user specified initial values for the parameters, use them if compile_args is not None and compile_args[ "initial_values"] is not None: # Assign them in the order given by self.second toVariable = [self.second[i] for i in self.second ] # Variable names in the correct order self.weight_variable.assign([ compile_args["initial_values"][val] for val in toVariable ]) else: self.weight_variable = None # Store extra useful information self._input_len = 0 if input_vars: self._input_len = len(self.input_vars) self._params_len = len(list(self.weight_vars)) self.samples = None if self.compile_args is not None: self.samples = self.compile_args["samples"]