diff --git a/doc/source/qip-basics.rst b/doc/source/qip-basics.rst index 52636cfe4..b5859dee2 100644 --- a/doc/source/qip-basics.rst +++ b/doc/source/qip-basics.rst @@ -68,7 +68,7 @@ full dimension of the circuit: .. testcode:: - U_list = qc.propagators() + U_list = qc.propagators(ignore_measurement=True) print(U_list) **Output**: @@ -103,7 +103,7 @@ can be achieved with the argument ``expand=False`` specified to the .. testcode:: - U_list = qc.propagators(expand=False) + U_list = qc.propagators(expand=False, ignore_measurement=True) print(U_list) **Output**: diff --git a/src/qutip_qip/circuit.py b/src/qutip_qip/circuit.py index 4e045057b..ba56ed231 100644 --- a/src/qutip_qip/circuit.py +++ b/src/qutip_qip/circuit.py @@ -33,7 +33,7 @@ from collections.abc import Iterable from itertools import product import numbers - +import warnings import inspect import numpy as np @@ -202,7 +202,7 @@ def __init__(self, N, input_states=None, output_states=None, self.N = N self.reverse_states = reverse_states self.gates = [] - self.dims = dims + self.dims = dims if dims is not None else [2] * N self.num_cbits = num_cbits if input_states: @@ -1247,7 +1247,7 @@ def adjacent_gates(self): return temp - def propagators(self, expand=True): + def propagators(self, expand=True, ignore_measurement=False): """ Propagator matrix calculator returning the individual steps as unitary matrices operating from left to right. @@ -1262,217 +1262,36 @@ def propagators(self, expand=True): list of unitaries will need to be combined with the list of gates in order to determine which qubits the unitaries should act on. - - Returns - ------- - U_list : list - Return list of unitary matrices for the qubit circuit. - """ - - if not expand: - return self._propagators_no_expand() - - U_list = [] - - gates = filter(lambda x: isinstance(x, Gate), self.gates) - - for gate in gates: - if gate.name == "RX": - U_list.append(rx( - gate.arg_value, self.N, gate.targets[0])) - elif gate.name == "RY": - U_list.append(ry( - gate.arg_value, self.N, gate.targets[0])) - elif gate.name == "RZ": - U_list.append(rz( - gate.arg_value, self.N, gate.targets[0])) - elif gate.name == "X": - U_list.append(x_gate(self.N, gate.targets[0])) - elif gate.name == "Y": - U_list.append(y_gate(self.N, gate.targets[0])) - elif gate.name == "CY": - U_list.append(cy_gate( - self.N, gate.controls[0], gate.targets[0])) - elif gate.name == "Z": - U_list.append(z_gate(self.N, gate.targets[0])) - elif gate.name == "CZ": - U_list.append(cz_gate( - self.N, gate.controls[0], gate.targets[0])) - elif gate.name == "T": - U_list.append(t_gate(self.N, gate.targets[0])) - elif gate.name == "CT": - U_list.append(ct_gate( - self.N, gate.controls[0], gate.targets[0])) - elif gate.name == "S": - U_list.append(s_gate(self.N, gate.targets[0])) - elif gate.name == "CS": - U_list.append(cs_gate( - self.N, gate.controls[0], gate.targets[0])) - elif gate.name == "SQRTNOT": - U_list.append(sqrtnot(self.N, gate.targets[0])) - elif gate.name == "SNOT": - U_list.append(snot(self.N, gate.targets[0])) - elif gate.name == "PHASEGATE": - U_list.append(phasegate(gate.arg_value, self.N, - gate.targets[0])) - elif gate.name == "QASMU": - U_list.append(qasmu_gate(gate.arg_value, self.N, - gate.targets[0])) - elif gate.name == "CRX": - U_list.append(controlled_gate(rx(gate.arg_value), - N=self.N, - control=gate.controls[0], - target=gate.targets[0])) - elif gate.name == "CRY": - U_list.append(controlled_gate(ry(gate.arg_value), - N=self.N, - control=gate.controls[0], - target=gate.targets[0])) - elif gate.name == "CRZ": - U_list.append(controlled_gate(rz(gate.arg_value), - N=self.N, - control=gate.controls[0], - target=gate.targets[0])) - elif gate.name == "CPHASE": - U_list.append(cphase(gate.arg_value, self.N, - gate.controls[0], gate.targets[0])) - elif gate.name == "CNOT": - U_list.append(cnot(self.N, - gate.controls[0], gate.targets[0])) - elif gate.name == "CSIGN": - U_list.append(csign(self.N, - gate.controls[0], gate.targets[0])) - elif gate.name == "BERKELEY": - U_list.append(berkeley(self.N, gate.targets)) - elif gate.name == "SWAPalpha": - U_list.append(swapalpha(gate.arg_value, self.N, - gate.targets)) - elif gate.name == "SWAP": - U_list.append(swap(self.N, gate.targets)) - elif gate.name == "ISWAP": - U_list.append(iswap(self.N, gate.targets)) - elif gate.name == "SQRTSWAP": - U_list.append(sqrtswap(self.N, gate.targets)) - elif gate.name == "SQRTISWAP": - U_list.append(sqrtiswap(self.N, gate.targets)) - elif gate.name == "FREDKIN": - U_list.append(fredkin(self.N, gate.controls[0], - gate.targets)) - elif gate.name == "TOFFOLI": - U_list.append(toffoli(self.N, gate.controls, - gate.targets[0])) - elif gate.name == "GLOBALPHASE": - U_list.append(globalphase(gate.arg_value, self.N)) - elif gate.name == "IDLE": - U_list.append(qeye(self.N * [2])) - 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_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 is neither function nor operator") - 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)) - - return U_list - - def _propagators_no_expand(self): - """ - Propagator matrix calculator for N qubits returning the individual - steps as unitary matrices operating from left to right. + ignore_measurement: bool, optional + Whether :class:`.Measurement` operators should be ignored. + If set False, it will raise an error + when the circuit has measurement. Returns ------- U_list : list Return list of unitary matrices for the qubit circuit. + Notes + ----- + If ``expand=False``, the global phase gate only returns a number. + Also, classical controls are be ignored. """ U_list = [] - gates = filter(lambda x: isinstance(x, Gate), self.gates) + gates = [g for g in self.gates if not isinstance(g, Measurement)] + if len(gates) < len(self.gates) and not ignore_measurement: + raise TypeError( + "Cannot compute the propagator of a measurement operator." + "Please set ignore_measurement=True.") for gate in gates: - if gate.name == "RX": - U_list.append(rx(gate.arg_value)) - elif gate.name == "RY": - U_list.append(ry(gate.arg_value)) - elif gate.name == "RZ": - U_list.append(rz(gate.arg_value)) - elif gate.name == "X": - U_list.append(x_gate()) - elif gate.name == "Y": - U_list.append(y_gate()) - elif gate.name == "CY": - U_list.append(cy_gate()) - elif gate.name == "Z": - U_list.append(z_gate()) - elif gate.name == "CZ": - U_list.append(cz_gate()) - elif gate.name == "T": - U_list.append(t_gate()) - elif gate.name == "CT": - U_list.append(ct_gate()) - elif gate.name == "S": - U_list.append(s_gate()) - elif gate.name == "CS": - U_list.append(cs_gate()) - elif gate.name == "SQRTNOT": - U_list.append(sqrtnot()) - elif gate.name == "SNOT": - U_list.append(snot()) - elif gate.name == "PHASEGATE": - U_list.append(phasegate(gate.arg_value)) - elif gate.name == "QASMU": - U_list.append(qasmu_gate(gate.arg_value)) - elif gate.name == "CRX": - U_list.append(controlled_gate(rx(gate.arg_value))) - elif gate.name == "CRY": - U_list.append(controlled_gate(ry(gate.arg_value))) - elif gate.name == "CRZ": - U_list.append(controlled_gate(rz(gate.arg_value))) - elif gate.name == "CPHASE": - U_list.append(cphase(gate.arg_value)) - elif gate.name == "CNOT": - U_list.append(cnot()) - elif gate.name == "CSIGN": - U_list.append(csign()) - elif gate.name == "BERKELEY": - U_list.append(berkeley()) - elif gate.name == "SWAPalpha": - U_list.append(swapalpha(gate.arg_value)) - elif gate.name == "SWAP": - U_list.append(swap()) - elif gate.name == "ISWAP": - U_list.append(iswap()) - elif gate.name == "SQRTSWAP": - U_list.append(sqrtswap()) - elif gate.name == "SQRTISWAP": - U_list.append(sqrtiswap()) - elif gate.name == "FREDKIN": - U_list.append(fredkin()) - elif gate.name == "TOFFOLI": - U_list.append(toffoli()) - elif gate.name == "GLOBALPHASE": - U_list.append(globalphase(gate.arg_value, self.N)) - elif gate.name == "IDLE": - U_list.append(qeye(2)) - elif gate.name in self.user_gates: + if gate.name == "GLOBALPHASE": + qobj = gate.get_qobj(self.N) + U_list.append(qobj) + continue + + if 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)) @@ -1481,21 +1300,26 @@ def _propagators_no_expand(self): func = func_or_oper para_num = len(inspect.getfullargspec(func)[0]) if para_num == 0: - oper = func() + qobj = func() elif para_num == 1: - oper = func(gate.arg_value) + qobj = func(gate.arg_value) else: raise ValueError( "gate function takes at most one parameters.") elif isinstance(func_or_oper, Qobj): - oper = func_or_oper + qobj = func_or_oper else: raise ValueError("gate is neither function nor operator") - U_list.append(oper) + if expand: + all_targets = gate.get_all_qubits() + qobj = expand_operator( + qobj, N=self.N, targets=all_targets, dims=self.dims) else: - raise NotImplementedError( - "{} gate is an unknown gate.".format(gate.name)) - + if expand: + qobj = gate.get_qobj(self.N, self.dims) + else: + qobj = gate.get_compact_qobj() + U_list.append(qobj) return U_list def compute_unitary(self): @@ -1824,9 +1648,10 @@ def __init__(self, qc, state=None, cbits=None, if U_list: self.U_list = U_list elif precompute_unitary: - self.U_list = qc.propagators(expand=False) + self.U_list = qc.propagators( + expand=False, ignore_measurement=True) else: - self.U_list = qc.propagators() + self.U_list = qc.propagators(ignore_measurement=True) self.ops = [] self.inds_list = [] @@ -1881,7 +1706,7 @@ def _process_ops_precompute(self): if isinstance(gate, Measurement): continue else: - self.inds_list.append(gate.get_inds(self.qc.N)) + self.inds_list.append(gate.get_all_qubits()) for operation in self.qc.gates: if isinstance(operation, Measurement): @@ -2073,7 +1898,7 @@ def step(self): if apply_gate: if self.precompute_unitary: U = expand_operator(U, self.qc.N, - operation.get_inds(self.qc.N)) + operation.get_all_qubits()) self._evolve_state(U) else: self._evolve_state(op) diff --git a/src/qutip_qip/operations/gates.py b/src/qutip_qip/operations/gates.py index 77ff50194..85a9e7d15 100644 --- a/src/qutip_qip/operations/gates.py +++ b/src/qutip_qip/operations/gates.py @@ -174,13 +174,25 @@ def __init__(self, name, targets=None, controls=None, self.arg_value = arg_value self.arg_label = arg_label - def get_inds(self, N=None): - if self.controls: + def get_all_qubits(self): + """ + Return a list of all qubits that the gate operator + acts on. + The list concatenates the two lists representing + the controls and the targets qubits while retains the order. + + Returns + ------- + targets_list : list of int + A list of all qubits, including controls and targets. + """ + if self.controls is not None: return self.controls + self.targets - if self.targets: + if self.targets is not None: return self.targets else: - return list(range(N)) + # Special case: the global phase gate + return [] def __str__(self): str_name = (("Gate(%s, targets=%s, controls=%s," @@ -222,6 +234,125 @@ def _to_qasm(self, qasm_out): self.targets, self.arg_value)) + def get_compact_qobj(self): + """ + Get the compact :class:`qutip.Qobj` representation of the gate + operator, ignoring the controls and targets. + In the unitary representation, + it always assumes that the first few qubits are controls, + then targets. + + Returns + ------- + qobj : :obj:`qutip.Qobj` + The compact gate operator as a unitary matrix. + """ + # TODO This will be moved to each sub-class of Gate + if self.name == "RX": + qobj = rx(self.arg_value) + elif self.name == "RY": + qobj = ry(self.arg_value) + elif self.name == "RZ": + qobj = rz(self.arg_value) + elif self.name == "X": + qobj = x_gate() + elif self.name == "Y": + qobj = y_gate() + elif self.name == "CY": + qobj = cy_gate() + elif self.name == "Z": + qobj = z_gate() + elif self.name == "CZ": + qobj = cz_gate() + elif self.name == "T": + qobj = t_gate() + elif self.name == "CT": + qobj = ct_gate() + elif self.name == "S": + qobj = s_gate() + elif self.name == "CS": + qobj = cs_gate() + elif self.name == "SQRTNOT": + qobj = sqrtnot() + elif self.name == "SNOT": + qobj = snot() + elif self.name == "PHASEGATE": + qobj = phasegate(self.arg_value) + elif self.name == "QASMU": + qobj = qasmu_gate(self.arg_value) + elif self.name == "CRX": + qobj = controlled_gate(rx(self.arg_value)) + elif self.name == "CRY": + qobj = controlled_gate(ry(self.arg_value)) + elif self.name == "CRZ": + qobj = controlled_gate(rz(self.arg_value)) + elif self.name == "CPHASE": + qobj = cphase(self.arg_value) + elif self.name == "CNOT": + qobj = cnot() + elif self.name == "CSIGN": + qobj = csign() + elif self.name == "BERKELEY": + qobj = berkeley() + elif self.name == "SWAPalpha": + qobj = swapalpha(self.arg_value) + elif self.name == "SWAP": + qobj = swap() + elif self.name == "ISWAP": + qobj = iswap() + elif self.name == "SQRTSWAP": + qobj = sqrtswap() + elif self.name == "SQRTISWAP": + qobj = sqrtiswap() + elif self.name == "FREDKIN": + qobj = fredkin() + elif self.name == "TOFFOLI": + qobj = toffoli() + elif self.name == "IDLE": + qobj = qeye(2) + elif self.name == "GLOBALPHASE": + raise NotImplementedError( + "Globalphase gate has no compack qobj representation.") + else: + raise NotImplementedError(f"{self.name} is an unknown gate.") + return qobj + + def get_qobj(self, num_qubits=None, dims=None): + """ + Get the :class:`qutip.Qobj` representation of the gate operator. + The operator is expanded to the full Herbert space according to + the controls and targets qubits defined for the gate. + + Parameters + ---------- + num_qubits : int, optional + The number of qubits. + If not given, use the minimal number of qubits required + by the target and control qubits. + dims : list, optional + A list representing the dimensions of each quantum system. + If not given, it is assumed to be an all-qubit system. + + Returns + ------- + qobj : :obj:`qutip.Qobj` + The compact gate operator as a unitary matrix. + """ + if self.name == "GLOBALPHASE": + if num_qubits is not None: + return globalphase(self.arg_value, num_qubits) + else: + raise ValueError( + "The number of qubits must be provided for " + "global phase gates.") + + all_targets = self.get_all_qubits() + if num_qubits is None: + num_qubits = max(all_targets) + return expand_operator( + self.get_compact_qobj(), N=num_qubits, targets=all_targets, + dims=dims) + _GATE_NAME_TO_LABEL = { 'X': r'X', @@ -1301,7 +1432,6 @@ def _gate_sequence_product(U_list, ind_list): >>> U_list, overall_inds = _gate_sequence_product(tensor_lst, overall_inds) """ - num_qubits = len(set(chain(*ind_list))) sorted_inds = sorted(set(_flatten(ind_list))) ind_list = [[sorted_inds.index(ind) for ind in inds] for inds in ind_list] @@ -1402,7 +1532,6 @@ def gate_sequence_product(U_list, left_to_right=True, overall_inds : list of int, optional List of qubit indices on which U_overall applies. """ - if expand: return _gate_sequence_product(U_list, inds_list) else: diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 5e149d1f6..59a067b89 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -620,7 +620,7 @@ def test_gate_product(self): if isinstance(gate, Measurement): continue else: - inds_list.append(gate.get_inds(qc.N)) + inds_list.append(gate.get_all_qubits()) U_1, _ = gate_sequence_product(U_list, inds_list=inds_list,