diff --git a/MANIFEST.in b/MANIFEST.in index 818ded2059..f1a9198a39 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,3 +8,4 @@ recursive-include qutip *.pxi recursive-include qutip *.hpp recursive-include qutip *.pxd recursive-include qutip *.ini +recursive-include qutip/tests/qasm_files *.qasm diff --git a/qutip/measurement.py b/qutip/measurement.py index 28bde51174..646d2f075e 100644 --- a/qutip/measurement.py +++ b/qutip/measurement.py @@ -362,7 +362,7 @@ def measure_povm(state, ops, targets=None): """ collapsed_states, probabilities = measurement_statistics_povm(state, - ops, targets) + ops, targets) index = np.random.choice(range(len(collapsed_states)), p=probabilities) state = collapsed_states[index] return index, state diff --git a/qutip/qip/circuit.py b/qutip/qip/circuit.py index 1d9ef10ec3..bb19c1ee76 100644 --- a/qutip/qip/circuit.py +++ b/qutip/qip/circuit.py @@ -45,8 +45,8 @@ from qutip.qip.operations.gates import (rx, ry, rz, sqrtnot, snot, phasegate, x_gate, y_gate, z_gate, cy_gate, cz_gate, s_gate, t_gate, cs_gate, - ct_gate, cphase, cnot, csign, - berkeley, swapalpha, swap, iswap, + qasmu_gate, ct_gate, cphase, cnot, + csign, berkeley, swapalpha, swap, iswap, sqrtswap, sqrtiswap, fredkin, toffoli, controlled_gate, globalphase, expand_operator) @@ -69,9 +69,9 @@ def DisplaySVG(data, *args, **kwargs): __all__ = ['Gate', 'QubitCircuit', 'Measurement'] _single_qubit_gates = ["RX", "RY", "RZ", "SNOT", "SQRTNOT", "PHASEGATE", - "X", "Y", "Z", "S", "T"] + "X", "Y", "Z", "S", "T", "QASMU"] _para_gates = ["RX", "RY", "RZ", "CPHASE", "SWAPalpha", "PHASEGATE", - "GLOBALPHASE", "CRX", "CRY", "CRZ"] + "GLOBALPHASE", "CRX", "CRY", "CRZ", "QASMU"] _ctrl_gates = ["CNOT", "CSIGN", "CRX", "CRY", "CRZ", "CY", "CZ", "CS", "CT"] _swap_like = ["SWAP", "ISWAP", "SQRTISWAP", "SQRTSWAP", "BERKELEY", @@ -93,16 +93,22 @@ class Gate: Gate targets. controls : list or int Gate controls. - classical_controls : list or int - Gate classical controls. Gate applied in circuit if all are 1. arg_value : float Argument value(phi). arg_label : string Label for gate representation. + classical_controls : int or list of int, optional + indices of classical bits to control gate on. + control_value : int, optional + value of classical bits to control on, the classical controls are + interpreted as an integer with lowest bit being the first one. + If not specified, then the value is interpreted to be + 2 ** len(classical_controls) - 1 (i.e. all classical controls are 1). """ - def __init__(self, name, targets=None, controls=None, arg_value=None, - arg_label=None, classical_controls=None): + def __init__(self, name, targets=None, controls=None, + arg_value=None, arg_label=None, + classical_controls=None, control_value=None): """ Create a gate with specified parameters. """ @@ -111,6 +117,7 @@ def __init__(self, name, targets=None, controls=None, arg_value=None, self.targets = None self.controls = None self.classical_controls = None + self.control_value = None if not isinstance(targets, Iterable) and targets is not None: self.targets = [targets] @@ -128,6 +135,10 @@ def __init__(self, name, targets=None, controls=None, arg_value=None, else: self.classical_controls = classical_controls + if (control_value is not None + and control_value < 2 ** len(classical_controls)): + self.control_value = control_value + for ind_list in [self.targets, self.controls, self.classical_controls]: if isinstance(ind_list, Iterable): all_integer = all( @@ -173,8 +184,10 @@ def __init__(self, name, targets=None, controls=None, arg_value=None, def __str__(self): str_name = (("Gate(%s, targets=%s, controls=%s," - " classical controls=%s)") % (self.name, self.targets, - self.controls, self.classical_controls)) + " classical controls=%s, control_value=%s)") + % (self.name, self.targets, + self.controls, self.classical_controls, + self.control_value)) return str_name def __repr__(self): @@ -203,6 +216,7 @@ def _repr_latex_(self): 'SQRTNOT': r'\sqrt{\rm NOT}', 'SNOT': r'{\rm H}', 'PHASEGATE': r'{\rm PHASE}', + 'QASMU': r'{\rm QASM-U}', 'CPHASE': r'{\rm R}', 'CNOT': r'{\rm CNOT}', 'CSIGN': r'{\rm Z}', @@ -307,7 +321,7 @@ def measurement_comp_basis(self, state): targets=self.targets) def __str__(self): - str_name = (("Measurement(%s, target=%s, classical_store=%s") % + str_name = (("Measurement(%s, target=%s, classical_store=%s)") % (self.name, self.targets, self.classical_store)) return str_name @@ -443,7 +457,8 @@ def add_measurement(self, measurement, targets=None, index=None, classical_store=classical_store)) def add_gate(self, gate, targets=None, controls=None, arg_value=None, - arg_label=None, index=None, classical_controls=None): + arg_label=None, index=None, + classical_controls=None, control_value=None): """ Adds a gate with specified parameters to the circuit. @@ -462,9 +477,14 @@ def add_gate(self, gate, targets=None, controls=None, arg_value=None, Label for gate representation. index : list Positions to add the gate. - classical_controls: list - Classical Controls for Gate. If multiple controls, - it applies gate when all are 1. + classical_controls : int or list of int, optional + indices of classical bits to control gate on. + control_value : int, optional + value of classical bits to control on, the classical controls are + interpreted as an integer with lowest bit being the first one. + If not specified, then the value is interpreted to be + 2 ** len(classical_controls) - 1 + (i.e. all classical controls are 1). """ if isinstance(gate, Gate): @@ -474,6 +494,7 @@ def add_gate(self, gate, targets=None, controls=None, arg_value=None, arg_value = gate.arg_value arg_label = gate.arg_label classical_controls = gate.classical_controls + control_value = gate.control_value else: name = gate @@ -481,7 +502,8 @@ def add_gate(self, gate, targets=None, controls=None, arg_value=None, if index is None: gate = Gate(name, targets=targets, controls=controls, arg_value=arg_value, arg_label=arg_label, - classical_controls=classical_controls) + classical_controls=classical_controls, + control_value=control_value) self.gates.append(gate) else: @@ -490,11 +512,13 @@ def add_gate(self, gate, targets=None, controls=None, arg_value=None, in self.gates[:position])) gate = Gate(name, targets=targets, controls=controls, arg_value=arg_value, arg_label=arg_label, - classical_controls=classical_controls) + classical_controls=classical_controls, + control_value=control_value) self.gates.insert(position, gate) def add_1q_gate(self, name, start=0, end=None, qubits=None, - arg_value=None, arg_label=None, classical_controls=None): + arg_value=None, arg_label=None, + classical_controls=None, control_value=None): """ Adds a single qubit gate with specified parameters on a variable number of qubits in the circuit. By default, it applies the given gate @@ -516,14 +540,15 @@ def add_1q_gate(self, name, start=0, end=None, qubits=None, Label for gate representation. """ if name not in ["RX", "RY", "RZ", "SNOT", "SQRTNOT", "PHASEGATE", - "X", "Y", "Z", "S", "T"]: + "X", "Y", "Z", "S", "T", "QASMU"]: raise ValueError("%s is not a single qubit gate" % name) if qubits is not None: for _, i in enumerate(qubits): gate = Gate(name, targets=qubits[i], controls=None, arg_value=arg_value, arg_label=arg_label, - classical_controls=classical_controls) + classical_controls=classical_controls, + control_value=control_value) self.gates.append(gate) else: @@ -532,7 +557,8 @@ def add_1q_gate(self, name, start=0, end=None, qubits=None, for i in range(start, end+1): gate = Gate(name, targets=i, controls=None, arg_value=arg_value, arg_label=arg_label, - classical_controls=classical_controls) + classical_controls=classical_controls, + control_value=control_value) self.gates.append(gate) def add_circuit(self, qc, start=0): @@ -556,7 +582,7 @@ def add_circuit(self, qc, start=0): gate = circuit_op if gate.name in ["RX", "RY", "RZ", - "SNOT", "SQRTNOT", "PHASEGATE"]: + "SNOT", "SQRTNOT", "PHASEGATE", "QASMU"]: self.add_gate(gate.name, gate.targets[0] + start, None, gate.arg_value, gate.arg_label) elif gate.name in ["X", "Y", "Z", "S", "T"]: @@ -706,6 +732,7 @@ def _gate_PHASEGATE(self, gate, temp_resolved): def _gate_NOTIMPLEMENTED(self, gate, temp_resolved): raise NotImplementedError("Cannot be resolved in this basis") + _gate_PHASEGATE = _gate_BERKELEY = _gate_SWAPalpha = _gate_NOTIMPLEMENTED _gate_SQRTSWAP = _gate_SQRTISWAP = _gate_NOTIMPLEMENTED @@ -1081,9 +1108,7 @@ def run(self, state, cbits=None, U_list=None, measure_results=()): i = int(measure_results[measure_ind]) measure_ind += 1 else: - i = np.random.choice( - [0, 1], - p=[probabilities[0], 1 - probabilities[0]]) + i = np.random.choice([0, 1], p=probabilities) probability *= probabilities[i] state = states[i] if operation.classical_store is not None: @@ -1092,8 +1117,15 @@ def run(self, state, cbits=None, U_list=None, measure_results=()): else: if (operation.classical_controls): - apply_gate = all([self.cbits[i] for i - in operation.classical_controls]) + if operation.control_value is None: + apply_gate = all([self.cbits[i] for i + in operation.classical_controls]) + else: + bitstring = "".join([str( + self.cbits[i]) for i + in operation.classical_controls[::-1]]) + register_value = int(bitstring, 2) + apply_gate = (register_value == operation.control_value) if apply_gate: state = self.U_list[ulistindex] * state ulistindex += 1 @@ -1321,8 +1353,8 @@ def adjacent_gates(self): # qubit to bring them closer. temp.gates.append(Gate("SWAP", targets=[i, i + 1])) temp.gates.append(Gate("SWAP", - targets=[start + end - i - 1, - start + end - i])) + targets=[start + end - i - 1, + start + end - i])) i += 1 elif gate.name in swap_gates: @@ -1342,8 +1374,8 @@ def adjacent_gates(self): else: temp.gates.append(Gate("SWAP", targets=[i, i + 1])) temp.gates.append(Gate("SWAP", - targets=[start + end - i - 1, - start + end - i])) + targets=[start + end - i - 1, + start + end - i])) i += 1 else: @@ -1409,6 +1441,9 @@ def propagators(self): elif gate.name == "PHASEGATE": self.U_list.append(phasegate(gate.arg_value, self.N, gate.targets[0])) + elif gate.name == "QASMU": + self.U_list.append(qasmu_gate(gate.arg_value, self.N, + gate.targets[0])) elif gate.name == "CRX": self.U_list.append(controlled_gate(rx(gate.arg_value), N=self.N, @@ -1456,21 +1491,25 @@ def propagators(self): self.U_list.append(globalphase(gate.arg_value, self.N)) elif gate.name in self.user_gates: if gate.controls is not None: - raise ValueError( - "A user defined gate {} takes only " - "`targets` variable.".format(gate.name)) - func = self.user_gates[gate.name] - para_num = len(inspect.getfullargspec(func)[0]) - if para_num == 0: - oper = func() - elif para_num == 1: - oper = func(gate.arg_value) + raise ValueError("A user defined gate {} takes only " + "`targets` variable.".format(gate.name)) + func_or_oper = self.user_gates[gate.name] + if inspect.isfunction(func_or_oper): + func = func_or_oper + para_num = len(inspect.getfullargspec(func)[0]) + if para_num == 0: + oper = func() + elif para_num == 1: + oper = func(gate.arg_value) + else: + raise ValueError( + "gate function takes at most one parameters.") + elif isinstance(func_or_oper, Qobj): + oper = func_or_oper else: - raise ValueError( - "gate function takes at most one parameters.") + raise ValueError("gate is neither function nor operator") self.U_list.append(expand_operator( oper, N=self.N, targets=gate.targets, dims=self.dims)) - else: raise NotImplementedError( "{} gate is an unknown gate.".format(gate.name)) diff --git a/qutip/qip/operations/gates.py b/qutip/qip/operations/gates.py index efaf06d889..7617a39952 100644 --- a/qutip/qip/operations/gates.py +++ b/qutip/qip/operations/gates.py @@ -46,7 +46,7 @@ __all__ = ['rx', 'ry', 'rz', 'sqrtnot', 'snot', 'phasegate', 'qrot', 'x_gate', 'y_gate', 'z_gate', 'cy_gate', 'cz_gate', 's_gate', - 't_gate', 'cs_gate', 'ct_gate', 'cphase', 'cnot', + 't_gate', 'qasmu_gate', 'cs_gate', 'ct_gate', 'cphase', 'cnot', 'csign', 'berkeley', 'swapalpha', 'swap', 'iswap', 'sqrtswap', 'sqrtiswap', 'fredkin', 'molmer_sorensen', 'toffoli', 'rotation', 'controlled_gate', @@ -364,6 +364,38 @@ def qrot(theta, phi, N=None, target=0): ]) +def qasmu_gate(args, N=None, target=0): + """ + QASM U-gate as defined in the OpenQASM standard. + + Parameters + ---------- + + theta : float + The argument supplied to the last RZ rotation. + phi : float + The argument supplied to the middle RY rotation. + gamma : float + The argument supplied to the first RZ rotation. + N : int + Number of qubits in the system. + target : int + The index of the target qubit. + + Returns + ------- + qasmu_gate : :class:`qutip.Qobj` + Quantum object representation of the QASM U-gate as defined in the + OpenQASM standard. + """ + + theta, phi, gamma = args + if N is not None: + return expand_operator(qasmu_gate([theta, phi, gamma]), N=N, + targets=target) + return Qobj(rz(phi) * ry(theta) * rz(gamma)) + + # # 2 Qubit Gates # diff --git a/qutip/qip/qasm.py b/qutip/qip/qasm.py new file mode 100644 index 0000000000..7e75aa5dba --- /dev/null +++ b/qutip/qip/qasm.py @@ -0,0 +1,753 @@ +import re +import os +from itertools import chain +from copy import deepcopy +import warnings + +from math import pi +import numpy as np + +from qutip.qip import gate_sequence_product +from qutip.qip.circuit import QubitCircuit +from qutip.qip.operations.gates import controlled_gate, qasmu_gate, rz, snot + + +__all__ = ["read_qasm"] + + +class QasmGate: + ''' + Class which stores the gate definitions as specified in the QASM file. + ''' + + def __init__(self, name, gate_args, gate_regs): + self.name = name + self.gate_args = gate_args + self.gate_regs = gate_regs + self.gates_inside = [] + + +def _get_qiskit_gates(): + ''' + Create and return a dictionary containing custom gates needed + for "qiskit" mode. These include a subset of gates usually defined + in the file "qelib1.inc". + + Returns a dictionary mapping gate names to QuTiP gates. + ''' + + def u2(args): + return qasmu_gate([np.pi/2, args[0], args[1]]) + + def id(): + return qasmu_gate([0, 0, 0]) + + def sdg(): + return rz(-1 * np.pi/2) + + def tdg(): + return rz(-1 * np.pi/4) + + def cu3(args): + return controlled_gate(qasmu_gate([args])) + + def ch(): + return controlled_gate(snot()) + + return {"ch": ch, "tdg": tdg, "id": id, "u2": u2, "sdg": sdg, "cu3": cu3} + + +def _tokenize_line(command): + ''' + Tokenize a single line of QASM code. + + Parameters + ---------- + command : str + one line of QASM code to be broken into "tokens". + + Returns + ------- + tokens : list of str + the tokens corresponding to the qasm line taken as input. + ''' + + # for gates without arguments + if "(" not in command: + tokens = list(chain(*[a.split() for a in command.split(",")])) + tokens = [token.strip() for token in tokens] + # for classically controlled gates + elif re.match(r"\s*if\s*\(", command): + groups = re.match(r"\s*if\s*\((.*)\)\s*(.*)\s+\((.*)\)(.*)", command) + # for classically controlled gates with arguments + if groups: + tokens = ["if", "(", groups.group(1), ")"] + tokens_gate = _tokenize_line("{} ({}) {}".format(groups.group(2), + groups.group(3), + groups.group(4))) + tokens += tokens_gate + # for classically controlled gates without arguments + else: + groups = re.match(r"\s*if\s*\((.*)\)(.*)", command) + tokens = ["if", "(", groups.group(1), ")"] + tokens_gate = _tokenize_line(groups.group(2)) + tokens += tokens_gate + tokens = [token.strip() for token in tokens] + # for gates with arguments + else: + groups = re.match(r"(^.*?)\((.*)\)(.*)", command) + if not groups: + raise SyntaxError("QASM: Incorrect bracket formatting") + tokens = groups.group(1).split() + tokens.append("(") + tokens += groups.group(2).split(",") + tokens.append(")") + tokens += groups.group(3).split(",") + tokens = [token.strip() for token in tokens] + + return tokens + + +def _tokenize(token_cmds): + ''' + Tokenize QASM code for processing. + + Parameters + ---------- + token_cmds : list of str + lines of QASM code. + + Returns + ------- + tokens : list of (list of str) + list of tokens corresponding to each qasm line taken as input. + ''' + + processed_commands = [] + + for line in token_cmds: + + # carry out some pre-processing for convenience + for c in "[]()": + line = line.replace(c, " " + c + " ") + for c in "{}": + line = line.replace(c, " ; " + c + " ; ") + line_commands = line.split(";") + line_commands = list(filter(lambda x: x != "", line_commands)) + + for command in line_commands: + tokens = _tokenize_line(command) + processed_commands.append(tokens) + + return list(filter(lambda x: x != [], processed_commands)) + + +def _gate_processor(command): + ''' + Process tokens for a gate call statement separating them into args and regs. + Processes tokens from a "gate call" (e.g. rx(pi) q[0]) and returns the + tokens for the arguments and registers separtely. + ''' + + gate_args = [] + gate_regs = [] + tokens = command[1:] + reg_start = 0 + + # extract arguments + if "(" in tokens and ")" in tokens: + bopen = tokens.index("(") + bclose = tokens.index(")") + gate_args = tokens[bopen+1:bclose] + reg_start = bclose+1 + + # extract registers + gate_regs = tokens[reg_start:] + + return gate_args, gate_regs + + +class QasmProcessor: + ''' + Class which holds variables used in processing QASM code. + ''' + + def __init__(self, commands, mode="qiskit", version="2.0"): + self.qubit_regs = {} + self.cbit_regs = {} + self.num_qubits = 0 + self.num_cbits = 0 + self.qasm_gates = {} + self.mode = mode + self.version = version + self.predefined_gates = set(["CX", "U"]) + + if self.mode == "qiskit": + self.qiskitgates = set(["u3", "u2", "u1", "cx", "id", "x", "y", + "z", "h", "s", "sdg", "t", "tdg", "rx", + "ry", "rz", "cz", "cy", "ch", "ccx", "crz", + "cu1", "cu3"]) + self.predefined_gates = self.predefined_gates.union(self.qiskitgates) + + self.gate_names = deepcopy(self.predefined_gates) + for gate in self.predefined_gates: + self.qasm_gates[gate] = QasmGate("U", + ["alpha", "beta", "gamma"], + ["q"]) + self.commands = commands + + def _process_includes(self): + ''' + QASM allows for code to be specified in additional files with the + ".inc" extension, especially to specify gate definitions in terms of + the built-in gates. Process into tokens all the + additional files and insert it into previously processed list. + ''' + + prev_index = 0 + expanded_commands = [] + + for curr_index, command in enumerate(self.commands): + + if command[0] != "include": + continue + + filename = command[1].strip('"') + + if self.mode == "qiskit" and filename == "qelib1.inc": + continue + + if os.path.exists(filename): + with open(filename, "r") as f: + qasm_lines = [line.strip() for line + in f.read().splitlines()] + qasm_lines = list(filter( + lambda x: x[:2] != "//" and x != "", + qasm_lines)) + + expanded_commands = (expanded_commands + + command[prev_index:curr_index] + + _tokenize(qasm_lines)) + prev_index = curr_index + 1 + else: + raise ValueError(command[1] + ": such a file does not exist") + + expanded_commands += self.commands[prev_index:] + self.commands = expanded_commands + + def _initialize_pass(self): + ''' + Passes through the tokenized commands, create QasmGate objects for + each user-defined gate, process register declarations. + ''' + + gate_defn_mode = False + open_bracket_mode = False + + unprocessed = [] + + for num, command in enumerate(self.commands): + if gate_defn_mode: + if command[0] == "{": + gate_defn_mode = False + open_bracket_mode = True + gate_elems = [] + continue + else: + raise SyntaxError("QASM: incorrect bracket formatting") + elif open_bracket_mode: + if command[0] == "{": + raise SyntaxError("QASM: incorrect bracket formatting") + elif command[0] == "}": + if not curr_gate.gates_inside: + raise NotImplementedError("QASM: opaque gate {} are \ + not allowed, please define \ + or omit \ + them".format(curr_gate.name)) + open_bracket_mode = False + self.gate_names.add(curr_gate.name) + self.qasm_gates[curr_gate.name] = curr_gate + continue + elif command[0] in self.gate_names: + name = command[0] + gate_args, gate_regs = _gate_processor(command) + gate_added = self.qasm_gates[name] + curr_gate.gates_inside.append([name, + gate_args, + gate_regs]) + elif command[0] == "gate": + gate_name = command[1] + gate_args, gate_regs = _gate_processor(command[1:]) + curr_gate = QasmGate(gate_name, gate_args, gate_regs) + gate_defn_mode = True + + elif command[0] == "qreg": + groups = re.match(r"(.*)\[(.*)\]", "".join(command[1:])) + if groups: + qubit_name = groups.group(1) + num_regs = int(groups.group(2)) + self.qubit_regs[qubit_name] = list(range( + self.num_qubits, + self.num_qubits + num_regs)) + self.num_qubits += num_regs + else: + raise SyntaxError("QASM: incorrect bracket formatting") + + elif command[0] == "creg": + groups = re.match(r"(.*)\[(.*)\]", "".join(command[1:])) + if groups: + cbit_name = groups.group(1) + num_regs = int(groups.group(2)) + self.cbit_regs[cbit_name] = list(range(self.num_cbits, + self.num_cbits + num_regs)) + self.num_cbits += num_regs + else: + raise SyntaxError("QASM: incorrect bracket formatting") + elif command[0] == "reset": + raise NotImplementedError(("QASM: reset functionality " + "is not supported.")) + elif command[0] in ["barrier", "include"]: + continue + else: + unprocessed.append(num) + continue + + if open_bracket_mode: + raise SyntaxError("QASM: incorrect bracket formatting") + + self.commands = [self.commands[i] for i in unprocessed] + + def _custom_gate(self, qc_temp, gate_call): + ''' + Recursively process a custom-defined gate with specified arguments + to produce a dummy circuit with all the gates in the custom-defined + gate. + + Parameters + ---------- + + qc_temp: QubitCircuit + temporary circuit to process custom gate + gate_call: list of str + tokens corresponding to gate signature/call + ''' + + gate_name, args, regs = gate_call + gate = self.qasm_gates[gate_name] + args_map = {} + regs_map = {} + + # maps variables to supplied arguments, registers + for i, arg in enumerate(gate.gate_args): + args_map[arg] = eval(str(args[i])) + for i, reg in enumerate(gate.gate_regs): + regs_map[reg] = regs[i] + # process all the constituent gates with supplied arguments, registers + for call in gate.gates_inside: + + # create function call for the constituent gate + name, com_args, com_regs = call + + for arg, real_arg in args_map.items(): + com_args = [command.replace(arg.strip(), str(real_arg)) + for command in com_args] + for reg, real_reg in regs_map.items(): + com_regs = [command.replace(reg.strip(), str(real_reg)) + for command in com_regs] + com_args = [eval(arg) for arg in com_args] + + if name in self.predefined_gates: + qc_temp.user_gates = _get_qiskit_gates() + com_regs = [int(reg) for reg in com_regs] + self._add_predefined_gates(qc_temp, name, com_regs, com_args) + else: + self._custom_gate(qc_temp, [name, com_args, com_regs]) + + def _regs_processor(self, regs, reg_type): + ''' + Process register tokens: map them to the QubitCircuit indices + of the respective registers. + + Parameters + ---------- + regs : list of str + token list corresponding to qubit/cbit register invocations. + reg_type : str + reg_type can be "measure" or "gate" to specify type of required + processing. + + Returns + ------- + + regs list : list of (list of regs) + list of register sets to which circuit operations + are applied. + + ''' + + # turns messy tokens into appropriate form + # ['q', 'p', '[', '0', ']'] -> ['q', 'p[0]'] + regs = [reg.replace(" ", "") for reg in regs] + if "[" in regs: + prev_token = "" + new_regs = [] + open_bracket_mode = False + for token in regs: + if token == "[": + open_bracket_mode = True + elif open_bracket_mode: + if token == "]": + open_bracket_mode = False + reg_name = new_regs.pop() + new_regs.append(reg_name + "[" + reg_num + "]") + elif token.isdigit(): + reg_num = token + else: + raise SyntaxError("QASM: incorrect bracket formatting") + else: + new_regs.append(token) + if open_bracket_mode: + raise SyntaxError("QASM: incorrect bracket formatting") + regs = new_regs + + if reg_type == "measure": + + # processes register tokens of the form q[i] -> c[i] + groups = re.match(r"(.*)\[(.*)\]->(.*)\[(.*)\]", "".join(regs)) + if groups: + qubit_name = groups.group(1) + qubit_ind = int(groups.group(2)) + qubit_lst = self.qubit_regs[qubit_name] + if qubit_ind < len(qubit_lst): + qubit = qubit_lst[0] + qubit_ind + else: + raise ValueError("QASM: qubit index out of bounds") + cbit_name = groups.group(3) + cbit_ind = int(groups.group(4)) + cbit_lst = self.cbit_regs[cbit_name] + if cbit_ind < len(cbit_lst): + cbit = cbit_lst[0] + cbit_ind + else: + raise ValueError("QASM: cbit index out of bounds") + return [(qubit, cbit)] + # processes register tokens of the form q -> c + else: + qubit_name = regs[0] + cbit_name = regs[2] + qubits = self.qubit_regs[qubit_name] + cbits = self.cbit_regs[cbit_name] + if len(qubits) == len(cbits): + return zip(qubits, cbits) + else: + raise ValueError("QASM: qubit and cbit \ + register sizes are different") + else: + # processes gate tokens to create sets of registers to + # which the gates are applied. + new_regs = [] + expand = 0 + for reg in regs: + if "[" in reg: + groups = re.match(r"(.*)\[(.*)\]", "".join(reg)) + qubit_name = groups.group(1) + qubit_ind = int(groups.group(2)) + qubit_lst = self.qubit_regs[qubit_name] + if qubit_ind < len(qubit_lst): + qubit = qubit_lst[0] + qubit_ind + else: + qubit_name = reg + qubit = self.qubit_regs[qubit_name] + expand = len(qubit) + new_regs.append(qubit) + if expand: + return zip(*list(map( + lambda x: x if isinstance(x, list) else [x] * expand, + new_regs))) + else: + return [new_regs] + + def _add_qiskit_gates(self, qc, name, regs, args=None, + classical_controls=None, control_value=None): + """ + Add any gates that are pre-defined in qiskit-style exported + qasm file with included "qelib1.inc". + + Parameters + ---------- + qc : QubitCircuit + the circuit to which the gate is added. + name : str + name of gate to be added. + regs : list of int + list of qubit register indices to add gates to. + args : float, optional + value of args supplied to the gate. + classical_controls : list of int, optional + indices of classical bits to control gate on. + control_value : int, optional + value of classical bits to control on, the classical controls are + interpreted as an integer with lowest bit being the first one. + If not specified, then the value is interpreted to be + 2 ** len(classical_controls) - 1 + (i.e. all classical controls are 1). + """ + + gate_name_map_1q = {"x": "X", "y": "Y", "z": "Z", "h": "SNOT", + "t": "T", "s": "S", "sdg": "sdg", "tdg": "tdg", + "rx": "RX", "ry": "RY", "rz": "RZ"} + if len(args) == 0: + args = None + elif len(args) == 1: + args = args[0] + + if name == "u3": + qc.add_gate("QASMU", targets=regs[0], arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + elif name == "u2": + qc.add_gate("u2", targets=regs[0], arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + elif name == "u1": + qc.add_gate("RZ", targets=regs[0], arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + elif name == "cz": + qc.add_gate("CZ", targets=regs[1], controls=regs[0], + classical_controls=classical_controls, + control_value=control_value) + elif name == "cy": + qc.add_gate("CY", targets=regs[1], controls=regs[0], + classical_controls=classical_controls, + control_value=control_value) + elif name == "ch": + qc.add_gate("ch", targets=regs, + classical_controls=classical_controls, + control_value=control_value) + elif name == "ccx": + qc.add_gate("TOFFOLI", targets=regs[2], controls=regs[:2], + classical_controls=classical_controls, + control_value=control_value) + elif name == "crz": + qc.add_gate("CRZ", targets=regs[1], controls=regs[0], + classical_controls=classical_controls, + control_value=control_value) + elif name == "cu1": + qc.add_gate("CPHASE", targets=regs[1], controls=regs[0], + arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + elif name == "cu3": + qc.add_gate("QASMU", targets=regs[1], controls=regs[0], + arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + if name == "cx": + qc.add_gate("CNOT", targets=int(regs[1]), controls=int(regs[0]), + classical_controls=classical_controls, + control_value=control_value) + elif name in gate_name_map_1q: + if args == []: + args = None + qc.add_gate(gate_name_map_1q[name], targets=regs[0], arg_value=args, + classical_controls=classical_controls, + control_value=control_value) + + def _add_predefined_gates(self, qc, name, com_regs, com_args, + classical_controls=None, control_value=None): + """ + Add any gates that are pre-defined and/or inbuilt + in our circuit. + + Parameters + ---------- + qc : QubitCircuit + the circuit to which the gate is added. + name : str + name of gate to be added. + regs : list of int + list of qubit register indices to add gates to. + args : float, optional + value of args supplied to the gate. + classical_controls : list of int, optional + indices of classical bits to control gate on. + control_value : int, optional + value of classical bits to control on, the classical controls are + interpreted as an integer with lowest bit being the first one. + If not specified, then the value is interpreted to be + 2 ** len(classical_controls) - 1 + (i.e. all classical controls are 1). + """ + + if name == "CX": + qc.add_gate("CNOT", + targets=int(com_regs[1]), + controls=int(com_regs[0]), + classical_controls=classical_controls, + control_value=control_value) + elif name == "U": + qc.add_gate("QASMU", + targets=int(com_regs[0]), + arg_value=[float(arg) for arg in com_args], + classical_controls=classical_controls, + control_value=control_value) + elif name in self.qiskitgates and self.mode == "qiskit": + self._add_qiskit_gates(qc, name, com_regs, com_args, + classical_controls, control_value) + + def _gate_add(self, qc, command, custom_gates, + classical_controls=None, control_value=None): + ''' + Add gates to QubitCircuit from processed tokens, define custom gates + if necessary. + + Parameters + ---------- + qc: QubitCircuit + circuit object to which gate is added + command: list of str + list of tokens corresponding to gate application + custom_gates: {gate name : gate function or unitary} + dictionary of user gates defined for qutip + classical_controls : int or list of int, optional + indices of classical bits to control gate on. + control_value : int, optional + value of classical bits to control on, the classical controls are + interpreted as an integer with lowest bit being the first one. + If not specified, then the value is interpreted to be + 2 ** len(classical_controls) - 1 + (i.e. all classical controls are 1). + ''' + + args, regs = _gate_processor(command) + reg_set = self._regs_processor(regs, "gate") + + if args: + gate_name = "{}({})".format(command[0], ",".join(args)) + else: + gate_name = "{}".format(command[0]) + + # creates custom-gate (if required) using gate defn and provided args + if (command[0] not in self.predefined_gates + and command[0] not in custom_gates): + n = len(reg_set[0]) + qc_temp = QubitCircuit(n) + self._custom_gate(qc_temp, + [command[0], args, [str(i) for i in range(n)]]) + unitary_mat = gate_sequence_product(qc_temp.propagators()) + custom_gates[gate_name] = unitary_mat + + qc.user_gates = custom_gates + + # adds gate to the QubitCircuit + for regs in reg_set: + regs = [int(i) for i in regs] + if command[0] in self.predefined_gates: + args = [eval(arg) for arg in args] + self._add_predefined_gates( + qc, + command[0], + regs, + args, + classical_controls=classical_controls, + control_value=control_value) + else: + if not isinstance(regs, list): + regs = [regs] + qc.add_gate(gate_name, + targets=regs, + classical_controls=classical_controls, + control_value=control_value) + + def _final_pass(self, qc): + ''' + Take a blank circuit, add all the gates and measurements specified + by QASM. + ''' + + custom_gates = {} + if self.mode == "qiskit": + custom_gates = _get_qiskit_gates() + + for command in self.commands: + if command[0] in self.gate_names: + # adds gates to the QubitCircuit + self._gate_add(qc, command, custom_gates) + elif command[0] == "measure": + # adds measurement to the QubitCircuit + reg_set = self._regs_processor(command[1:], "measure") + for regs in reg_set: + qc.add_measurement("M", targets=[regs[0]], + classical_store=regs[1]) + elif command[0] == "if": + warnings.warn(("Information about individual registers" + " is not preserved in QubitCircuit")) + # adds classically controlled gates to the QubitCircuit + cbit_reg, control_value = command[2].split("==") + cbit_inds = self.cbit_regs[cbit_reg] + control_value = int(control_value) + self._gate_add(qc, command[4:], custom_gates, + cbit_inds, control_value) + else: + err = "QASM: {} is not a valid QASM command.".format(command[0]) + raise SyntaxError(err) + + +def read_qasm(qasm_input, mode="qiskit", version="2.0", strmode=False): + ''' + Read OpenQASM intermediate representation + (https://github.com/Qiskit/openqasm) and return + a QubitCircuit and state inputs as specified in the + QASM file. + + Parameters + ---------- + qasm_input : str + File location or String Input for QASM file to be imported. In case of + string input, the parameter strmode must be True. + mode : str + QASM mode to be read in. When mode is "qiskit", + the "qelib1.inc" include is automatically included, + without checking externally. Otherwise, each include is + processed. + version : str + QASM version of the QASM file. Only version 2.0 is currently supported. + strmode : bool + if specified as True, indicates that qasm_input is in string format + rather than from file. + + Returns + ------- + qc : QubitCircuit + Returns QubitCircuit specified in the QASM file. + ''' + + if strmode: + qasm_lines = qasm_input.splitlines() + else: + f = open(qasm_input, "r") + qasm_lines = f.read().splitlines() + f.close() + + # split input into lines and ignore comments + qasm_lines = [line.strip() for line in qasm_lines] + qasm_lines = list(filter(lambda x: x[:2] != "//" and x != "", qasm_lines)) + + if version != "2.0": + raise NotImplementedError("QASM: Only OpenQASM 2.0 \ + is currently supported.") + + if qasm_lines.pop(0) != "OPENQASM 2.0;": + raise SyntaxError("QASM: File does not contain QASM 2.0 header") + + qasm_obj = QasmProcessor(qasm_lines, mode=mode, version=version) + qasm_obj.commands = _tokenize(qasm_obj.commands) + + qasm_obj._process_includes() + + qasm_obj._initialize_pass() + qc = QubitCircuit(qasm_obj.num_qubits, num_cbits=qasm_obj.num_cbits) + + qasm_obj._final_pass(qc) + + return qc diff --git a/qutip/tests/qasm_files/bracket_error.qasm b/qutip/tests/qasm_files/bracket_error.qasm new file mode 100644 index 0000000000..1cc9d1e9c0 --- /dev/null +++ b/qutip/tests/qasm_files/bracket_error.qasm @@ -0,0 +1,16 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; +creg c[4]; +gate cu1fixed (a) c,t { +u1 (-a) t; +cx c,t; +u1 (a) t; +cx c,t; + +gate cu c,t { +cu1fixed (3*pi/8) c,t; +} + +h q[0]; diff --git a/qutip/tests/qasm_files/command_error.qasm b/qutip/tests/qasm_files/command_error.qasm new file mode 100644 index 0000000000..4efa7a43d4 --- /dev/null +++ b/qutip/tests/qasm_files/command_error.qasm @@ -0,0 +1,10 @@ +// Implementation of Deutsch algorithm with two qubits for f(x)=x +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[5]; +creg c[5]; + +h q[3]; +post q[3]; +measure q[3] -> c[3]; diff --git a/qutip/tests/qasm_files/qasm_error.qasm b/qutip/tests/qasm_files/qasm_error.qasm new file mode 100644 index 0000000000..2febf61ec5 --- /dev/null +++ b/qutip/tests/qasm_files/qasm_error.qasm @@ -0,0 +1,15 @@ +include "qelib1.inc"; + +qreg q[2]; +creg c[4]; +gate cu1fixed (a) c,t { +u1 (-a) t; +cx c,t; +u1 (a) t; +cx c,t; + +gate cu c,t { +cu1fixed (3*pi/8) c,t; +} + +h q[0]; diff --git a/qutip/tests/qasm_files/teleportation.qasm b/qutip/tests/qasm_files/teleportation.qasm new file mode 100644 index 0000000000..331e3bcf32 --- /dev/null +++ b/qutip/tests/qasm_files/teleportation.qasm @@ -0,0 +1,17 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[3]; +creg c0[1]; +creg c1[1]; + +h q[1]; +cx q[1], q[2]; +cx q[0], q[1]; +h q[0]; + +measure q[0] -> c1[0] +measure q[1] -> c0[0] + +if(c0==1) x q[2] +if(c1==1) z q[2] diff --git a/qutip/tests/qasm_files/test_add.qasm b/qutip/tests/qasm_files/test_add.qasm new file mode 100644 index 0000000000..2df3c9b79f --- /dev/null +++ b/qutip/tests/qasm_files/test_add.qasm @@ -0,0 +1,13 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; +creg c[2]; + +x q[1]; +h q[0]; +h q[1]; +cx q[0],q[1]; +h q; +if(c==0) h q[0] +measure q -> c; diff --git a/qutip/tests/qasm_files/test_custom_gates.qasm b/qutip/tests/qasm_files/test_custom_gates.qasm new file mode 100644 index 0000000000..fe15abba88 --- /dev/null +++ b/qutip/tests/qasm_files/test_custom_gates.qasm @@ -0,0 +1,22 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +gate u3alt(theta, gamma, alpha) a{ +rz(alpha) a; +ry(theta) a; +rz(gamma) a; +} + +gate testcu(theta, gamma, alpha) a, b { +rz(-gamma) b; +u3alt(theta, gamma, alpha) b; +rz(-alpha) b; +cx a, b; +} + +u3alt(pi/4, -pi/4, pi/4) q[1]; +U(pi/4, -pi/4, pi/4) q[1]; + +testcu(pi/2, pi/2, pi/2) q[0], q[1]; diff --git a/qutip/tests/test_qasm.py b/qutip/tests/test_qasm.py new file mode 100644 index 0000000000..757b41e452 --- /dev/null +++ b/qutip/tests/test_qasm.py @@ -0,0 +1,118 @@ +# This file is part of QuTiP: Quantum Toolbox in Python. +# +# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names +# of its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +############################################################################### + +import pytest +import numpy as np +from pathlib import Path + +from qutip.qip.qasm import read_qasm +from qutip.qip import Measurement +from qutip import tensor, rand_ket, basis, rand_dm, identity +from qutip.qip.operations.gates import cnot, ry + + +@pytest.mark.parametrize(["filename", "error", "error_message"], [ + pytest.param("command_error.qasm", SyntaxError, + "QASM: post is not a valid QASM command."), + pytest.param("bracket_error.qasm", SyntaxError, + "QASM: incorrect bracket formatting"), + pytest.param("qasm_error.qasm", SyntaxError, + "QASM: File does not contain QASM 2.0 header")]) +def test_qasm_errors(filename, error, error_message): + filepath = Path(__file__).parent / 'qasm_files' / filename + with pytest.raises(error) as exc_info: + read_qasm(filepath) + assert error_message in str(exc_info.value) + + +def check_gate_defn(gate, gate_name, targets, controls=None, + classical_controls=None, control_value=None): + assert gate.name == gate_name + assert gate.targets == targets + assert gate.controls == controls + assert gate.classical_controls == classical_controls + assert gate.control_value == control_value + + +def check_measurement_defn(gate, gate_name, targets, classical_store): + assert gate.name == gate_name + assert gate.targets == targets + assert gate.classical_store == classical_store + + +def test_qasm_addcircuit(): + filename = "test_add.qasm" + filepath = Path(__file__).parent / 'qasm_files' / filename + qc = read_qasm(filepath) + assert qc.N == 2 + assert qc.num_cbits == 2 + check_gate_defn(qc.gates[0], "X", [1]) + check_gate_defn(qc.gates[1], "SNOT", [0]) + check_gate_defn(qc.gates[2], "SNOT", [1]) + check_gate_defn(qc.gates[3], "CNOT", [1], [0]) + check_gate_defn(qc.gates[4], "SNOT", [0]) + check_gate_defn(qc.gates[5], "SNOT", [1]) + check_gate_defn(qc.gates[6], "SNOT", [0], None, [0, 1], 0) + check_measurement_defn(qc.gates[7], "M", [0], 0) + check_measurement_defn(qc.gates[8], "M", [1], 1) + + +def test_custom_gates(): + filename = "test_custom_gates.qasm" + filepath = Path(__file__).parent / 'qasm_files' / filename + qc = read_qasm(filepath) + unitaries = qc.propagators() + assert (unitaries[0] - unitaries[1]).norm() < 1e-12 + ry_cx = cnot() * tensor(identity(2), ry(np.pi/2)) + assert (unitaries[2] - ry_cx).norm() < 1e-12 + + +def test_qasm_teleportation(): + filename = "teleportation.qasm" + filepath = Path(__file__).parent / 'qasm_files' / filename + teleportation = read_qasm(filepath) + final_measurement = Measurement("start", targets=[2]) + initial_measurement = Measurement("start", targets=[0]) + + state = tensor(rand_ket(2), basis(2, 0), basis(2, 0)) + _, initial_probabilities = initial_measurement.measurement_comp_basis(state) + + states, probabilites = teleportation.run_statistics(state) + + for i, state in enumerate(states): + final = state + prob = probabilites[i] + _, final_probabilities = final_measurement.measurement_comp_basis(final) + np.testing.assert_allclose(initial_probabilities, + final_probabilities) + assert prob == pytest.approx(0.25, abs=1e-7) diff --git a/setup.py b/setup.py index c382042fe5..eb904c3bd1 100755 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ PACKAGE_DATA = { 'qutip': ['configspec.ini'], 'qutip/tests': ['*.ini'], + 'qutip/tests/qasm_files': ['*.qasm'], 'qutip/cy': ['*.pxi', '*.pxd', '*.pyx'], 'qutip/cy/src': ['*.cpp', '*.hpp'], 'qutip/control': ['*.pyx'],