diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index ca12f34a..615ed6e0 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -78,6 +78,8 @@ def runClientNode(qReg, virtRoot, myName, classicalNet): elif simulaqron_settings.sim_backend == "stabilizer": array, _ = yield virtRoot.callRemote("get_register_RI", q1) state = StabilizerState(array) + elif simulaqron_settings.sim_backend == "pyqrack": + state = '(Not directly queryable with PyQrack)' else: ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 3ed10d4f..946c60a0 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -121,6 +121,8 @@ def remote_recover_teleport(self, a, b, virtualNum): elif simulaqron_settings.sim_backend == "stabilizer": array, _, = yield self.virtRoot.callRemote("get_register_RI", eprB) state = StabilizerState(array) + elif simulaqron_settings.backend == "pyqrack": + state = '(Not directly queryable with PyQrack)' else: ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 22e39622..878008c9 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -60,7 +60,7 @@ def reset(save_loggers=False): def check_sim_backend(sim_backend): - if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: + if sim_backend in [SimBackend.PROJECTQ, SimBackend.PYQRACK, SimBackend.QUTIP]: assert has_module.main(sim_backend.value), f"To use {sim_backend} as backend you need to install the package" diff --git a/simulaqron/settings.py b/simulaqron/settings.py index d251c238..3993beab 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -44,6 +44,7 @@ class SimBackend(Enum): STABILIZER = "stabilizer" PROJECTQ = "projectq" + PYQRACK = "pyqrack" QUTIP = "qutip" diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index eab26efc..ae8171a9 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -233,7 +233,7 @@ def default(): @set.command() @click.argument('value', type=click.Choice([b.value for b in SimBackend])) def sim_backend(value): - """The backend to use (stabilizer, projectq, qutip).""" + """The backend to use (stabilizer, projectq, pyqrack, qutip).""" simulaqron_settings.sim_backend = value @@ -315,7 +315,7 @@ def get(): @get.command() def sim_backend(): - """The backend to use (stabilizer, projectq, qutip).""" + """The backend to use (stabilizer, projectq, pyqrack, qutip).""" print(simulaqron_settings.sim_backend) diff --git a/simulaqron/virtual_node/pyqrack_simulator.py b/simulaqron/virtual_node/pyqrack_simulator.py new file mode 100644 index 00000000..89d71d76 --- /dev/null +++ b/simulaqron/virtual_node/pyqrack_simulator.py @@ -0,0 +1,329 @@ +# +# Copyright (c) 2017, Stephanie Wehner and Axel Dahlberg +# 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. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Stephanie Wehner, QuTech. +# 4. Neither the name of the QuTech organization 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 ''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 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. + +try: + from pyqrack import QrackSimulator, Pauli +except ImportError: + raise RuntimeError("If you want to use the pyqrack backend you need to install the python package 'pyqrack'") +import numpy as np + +from simulaqron.virtual_node.basics import quantumEngine, quantumError, noQubitError + + +class pyqrackEngine(quantumEngine): + """ + Basic quantum engine which uses PyQrack. + + Attributes: + maxQubits: maximum number of qubits this engine will support. + """ + + def __init__(self, node, num, maxQubits=10): + """ + Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. + """ + + super().__init__(node=node, num=num, maxQubits=maxQubits) + + self.engine = QrackSimulator() + + # We start with no active qubits + self.activeQubits = 0 + self.nextQid = 0 + self.qubitReg = [] + + def add_fresh_qubit(self): + """ + Add a new qubit initialized in the \|0\> state. + """ + # Check if we are still allowed to add qubits + if self.activeQubits >= self.maxQubits: + raise noQubitError("No more qubits available in register.") + + # Prepare a clean qubit state in |0> + qid = self.nextQid + self.nextQid += 1 + self.engine.allocate_qubit(qid) + self.activeQubits += 1 + self.qubitReg.append(qid) + + return qid + + def add_qubit(self, newQubit): + """ + Add new qubit in the state described by the vector newQubit ([a, b]) + """ + + norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) + if not norm <= 1: + raise quantumError("State {} is not normalized.".format(newQubit)) + + # Create a fresh qubit + qid = self.add_fresh_qubit() + + # Find an appropriate state preparation gate + prob = np.dot(complex(newQubit[1]), np.conj(complex(newQubit[1]))) + sqrtProb = np.sqrt(prob) + sqrt1MinProb = np.sqrt(1 - prob) + + phase0 = 0 + if sqrt1MinProb > 0: + phase0 = complex(newQubit[0]) / sqrt1MinProb + + phase1 = 0 + if sqrtProb > 0: + phase1 = complex(newQubit[1]) / sqrt1MinProb + + cMtrx = [sqrt1MinProb * phase0, sqrtProb * phase0, sqrtProb * phase1, -sqrt1MinProb * phase1] + + # Transform the new qubit into the correct state + self.engine.mtrx(cMtrx, qid) + + return qid + + def validate_qid(self, qid): + if qid not in self.qubitReg: + raise quantumError("No such qubit to remove") + + def remove_qubit(self, qubitNum): + """ + Removes the qubit with the desired number qubitNum + """ + self.validate_qid(qubitNum) + + self.engine.release(qubitNum) + self.qubitReg.remove(qubitNum) + self.activeQubits -= 1 + + def get_register_RI(self): + """ + Retrieves the entire register in real and imaginary parts and returns the result as a + list. Twisted only likes to send real valued lists, not complex ones. + """ + raise NotImplementedError("get_register_RI() not implemented for this backend!") + + def apply_H(self, qubitNum): + """ + Applies a Hadamard gate to the qubits with number qubitNum. + """ + self.validate_qid(qubitNum) + + self.engine.h(qubitNum) + + def apply_K(self, qubitNum): + """ + Applies a K gate to the qubits with number qubitNum. Maps computational basis to Y eigenbasis. + """ + self.validate_qid(qubitNum) + + self.engine.h(qubitNum) + self.engine.s(qubitNum) + self.engine.h(qubitNum) + self.engine.z(qubitNum) + + def apply_X(self, qubitNum): + """ + Applies a X gate to the qubits with number qubitNum. + """ + self.validate_qid(qubitNum) + + self.engine.x(qubitNum) + + def apply_Z(self, qubitNum): + """ + Applies a Z gate to the qubits with number qubitNum. + """ + self.validate_qid(qubitNum) + + self.engine.z(qubitNum) + + def apply_Y(self, qubitNum): + """ + Applies a Y gate to the qubits with number qubitNum. + """ + self.validate_qid(qubitNum) + + self.engine.y(qubitNum) + + def apply_T(self, qubitNum): + """ + Applies a T gate to the qubits with number qubitNum. + """ + self.validate_qid(qubitNum) + + self.engine.t(qubitNum) + + def apply_rotation(self, qubitNum, n, a): + """ + Applies a rotation around the axis n with the angle a to qubit with number qubitNum. If n is zero a ValueError + is raised. + + :param qubitNum: int + Qubit number + :param n: tuple of floats + A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :param a: float + The rotation angle in radians. + """ + self.validate_qid(qubitNum) + + n = tuple(n) + if n == (1, 0, 0): + self.engine.r(Pauli.PauliX, a, qubitNum) + elif n == (0, 1, 0): + self.engine.r(Pauli.PauliY, a, qubitNum) + elif n == (0, 0, 1): + self.engine.r(Pauli.PauliZ, a, qubitNum) + else: + raise NotImplementedError("Can only do rotations around X, Y, or Z axis right now") + + def validate_control_qids(self, qid1, qid2): + if qid1 not in self.qubitReg: + raise quantumError("No such qubit to act as a control qubit") + + if qid2 not in self.qubitReg: + raise quantumError("No such qubit to act as a target qubit") + + if qid1 == qid2: + raise quantumError("Control and target are equal") + + def apply_CNOT(self, qubitNum1, qubitNum2): + """ + Applies the CNOT to the qubit with the numbers qubitNum1 and qubitNum2. + """ + self.validate_control_qids(qubitNum1, qubitNum2) + + self.engine.mcx([qubitNum1], qubitNum2) + + def apply_CPHASE(self, qubitNum1, qubitNum2): + """ + Applies the CPHASE to the qubit with the numbers qubitNum1 and qubitNum2. + """ + self.validate_control_qids(qubitNum1, qubitNum2) + + self.engine.mcz([qubitNum1], qubitNum2) + + def apply_onequbit_gate(self, gate, qubitNum): + """ + Applies a unitary gate to the specified qubit. + + Arguments: + gate The pyqrack gate to be applied + qubitNum the number of the qubit this gate is applied to + """ + self.validate_qid(qubitNum) + + self.engine.mtrx(gate, qubitNum) + + def apply_twoqubit_gate(self, gate, qubit1, qubit2): + """ + Applies a unitary gate to the two specified qubits. + + Arguments: + gate The pyqrack gate to be applied + qubit1 the first qubit + qubit2 the second qubit + """ + raise NotImplementedError("apply_twoqubit_gate() not implemented for this backend!") + + def measure_qubit_inplace(self, qubitNum): + """ + Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register + is in the post-measurment state corresponding to the obtained outcome. + + Arguments: + qubitNum qubit to be measured + """ + + # Check we have such a qubit... + self.validate_qid(qubitNum) + + outcome = self.engine.m(qubitNum) + + # return measurement outcome + return outcome + + def measure_qubit(self, qubitNum): + """ + Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. + + Arguments: + qubitNum qubit to be measured + """ + outcome = self.measure_qubit_inplace(qubitNum) + + self.remove_qubit(qubitNum) + + return outcome + + def replace_qubit(self, qubitNum, state): + """ + Replaces the qubit at position qubitNum with the one given by state. + """ + raise NotImplementedError("Currently you cannot replace a qubit using pyqrack as backend") + + def absorb(self, other): + """ + Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. + """ + + # Check whether there is space + newNum = self.activeQubits + other.activeQubits + if newNum > self.maxQubits: + raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + + # Check whether there are in fact qubits to tensor up.... + if self.activeQubits == 0: + self.engine = other.engine + self.qubitReg = list(other.qubitReg) + self.nextQid = other.nextQid + elif other.activeQubits > 0: + # PyQrack can internally "compose" the two engines together. + nQubits = [] + for q in range(other.activeQubits): + nQubits.append(self.nextQid) + self.nextQid += 1 + + self.engine.compose(other.engine, nQubits) + + # Add the qubits to the list of qubits + self.qubitReg += nQubits + + self.activeQubits = newNum + + def absorb_parts(self, R, I, activeQ): + """ + Absorb the qubits, given in pieces + + Arguments: + R real part of the qubit state as a list + I imaginary part as a list + activeQ active number of qubits + """ + raise NotImplementedError("Currently you cannot absorb_parts() using pyqrack as backend") diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 66a61873..819f4fb5 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -50,6 +50,8 @@ from simulaqron.virtual_node.qutip_simulator import qutipEngine elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: from simulaqron.virtual_node.project_q_simulator import projectQEngine +elif simulaqron_settings.sim_backend == SimBackend.PYQRACK.value: + from simulaqron.virtual_node.pyqrack_simulator import pyqrackEngine elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: from simulaqron.virtual_node.stabilizer_simulator import stabilizerEngine else: @@ -409,6 +411,8 @@ def remote_new_register(self, maxQubits=10): newReg = qutipEngine(self.myID, regNum, maxQubits) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: newReg = projectQEngine(self.myID, regNum, maxQubits) + elif simulaqron_settings.sim_backend == SimBackend.PYQRACK.value: + newReg = pyqrackEngine(self.myID, regNum, maxQubits) elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: newReg = stabilizerEngine(self.myID, regNum, maxQubits) else: diff --git a/tests/quick/engine/test_pyqrack_engine.py b/tests/quick/engine/test_pyqrack_engine.py new file mode 100644 index 00000000..3a54d902 --- /dev/null +++ b/tests/quick/engine/test_pyqrack_engine.py @@ -0,0 +1,321 @@ +import unittest +import numpy as np + +from simulaqron.toolbox import has_module +from simulaqron.settings import SimBackend + +if has_module.main(SimBackend.PYQRACK.value): + + from simulaqron.virtual_node.pyqrack_simulator import pyqrackEngine + from simulaqron.virtual_node.basics import noQubitError, quantumError + + _has_module = True + +else: + + _has_module = False + + +def if_has_module(test): + def new_test(self): + if _has_module: + test(self) + + return new_test + + +class TestPyQrackEngine_init(unittest.TestCase): + @if_has_module + def test_init(self): + eng = pyqrackEngine("Alice", 0) + self.assertEqual(eng.maxQubits, 10) + self.assertEqual(eng.activeQubits, 0) + self.assertEqual(len(eng.qubitReg), 0) + + eng = pyqrackEngine("Alice", 0, 5) + self.assertEqual(eng.maxQubits, 5) + self.assertEqual(eng.activeQubits, 0) + self.assertEqual(len(eng.qubitReg), 0) + + +class TestPyQrackEngine(unittest.TestCase): + @if_has_module + def setUp(self): + self.eng = pyqrackEngine("Alice", 0) + + @staticmethod + def abs_inner_product(state, ref): + comb_state = np.array(state[0]) + 1j * np.array(state[1]) + inner = np.dot(comb_state, np.array(ref).conj()) + return np.abs(inner) + + @if_has_module + def test_add_fresh_qubit(self): + num = self.eng.add_fresh_qubit() + self.assertEqual(num, 0) + self.assertEqual(self.eng.activeQubits, 1) + self.assertEqual(len(self.eng.qubitReg), 1) + self.assertTrue(isinstance(self.eng.qubitReg[num], int)) + + @if_has_module + def test_add_to_many_fresh_qubits(self): + for _ in range(10): + self.eng.add_fresh_qubit() + with self.assertRaises(noQubitError): + self.eng.add_fresh_qubit() + + @if_has_module + def test_add_qubit(self): + new_state = [1, 0] + num = self.eng.add_qubit(new_state) + self.assertEqual(num, 0) + self.assertEqual(self.eng.activeQubits, 1) + self.assertEqual(len(self.eng.qubitReg), 1) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [0] * 10) + + @if_has_module + def test_add_qubit_H(self): + new_state = [1 / np.sqrt(2), 1 / np.sqrt(2)] + num = self.eng.add_qubit(new_state) + self.assertEqual(num, 0) + self.assertEqual(self.eng.activeQubits, 1) + self.assertEqual(len(self.eng.qubitReg), 1) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [0] * 10) + + @if_has_module + def test_add_unphysical_qubit(self): + new_state = [1, 1] + with self.assertRaises(quantumError): + self.eng.add_qubit(new_state) + + @if_has_module + def test_remove_qubit(self): + num = self.eng.add_fresh_qubit() + self.eng.remove_qubit(num) + self.assertEqual(self.eng.activeQubits, 0) + self.assertEqual(len(self.eng.qubitReg), 0) + with self.assertRaises(quantumError): + self.eng.remove_qubit(num) + + @if_has_module + def test_get_register_RI(self): + self.eng.add_fresh_qubit() + self.eng.add_fresh_qubit() + with self.assertRaises(NotImplementedError): + self.eng.get_register_RI() + + @if_has_module + def test_H(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_H(num) + self.eng.engine.z(num) + self.eng.engine.h(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_K(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_K(num) + self.eng.apply_Z(num) + self.eng.apply_H(num) + self.eng.engine.adjs(num) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [0] * 10) + + @if_has_module + def test_X(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_X(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_Y(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_Y(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + self.eng.apply_H(num) + self.eng.apply_Y(num) + self.eng.engine.h(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [0] * 10) + + @if_has_module + def test_Z(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_H(num) + self.eng.apply_Z(num) + self.eng.engine.h(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_Rx(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_rotation(num, (1, 0, 0), np.pi / 2) + self.eng.engine.adjs(0) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_Ry(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_rotation(num, (0, 1, 0), np.pi / 2) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [0] * 10) + + @if_has_module + def test_Rz(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_H(num) + self.eng.apply_rotation(num, (0, 0, 1), np.pi / 2) + self.eng.engine.s(0) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_faulty_rot(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_H(num) + with self.assertRaises(NotImplementedError): + self.eng.apply_rotation(num, (1, 0, 1), np.pi / 2) + + @if_has_module + def test_cnot(self): + num1 = self.eng.add_fresh_qubit() + num2 = self.eng.add_fresh_qubit() + self.eng.apply_X(num1) + self.eng.apply_CNOT(num1, num2) + results = self.eng.engine.measure_shots([num1, num2], 10) + self.assertEqual(results, [3] * 10) + + @if_has_module + def test_cz(self): + num1 = self.eng.add_fresh_qubit() + num2 = self.eng.add_fresh_qubit() + self.eng.apply_X(num1) + self.eng.apply_H(num2) + self.eng.apply_CPHASE(num1, num2) + self.eng.apply_H(num2) + results = self.eng.engine.measure_shots([num1, num2], 10) + self.assertEqual(results, [3] * 10) + + @if_has_module + def test_measure0(self): + num = self.eng.add_fresh_qubit() + m = self.eng.measure_qubit(num) + self.assertEqual(m, 0) + self.assertEqual(self.eng.activeQubits, 0) + + @if_has_module + def test_measure1(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_X(num) + m = self.eng.measure_qubit(num) + self.assertEqual(m, 1) + self.assertEqual(self.eng.activeQubits, 0) + + @if_has_module + def test_measure_inplace(self): + num = self.eng.add_fresh_qubit() + m = self.eng.measure_qubit_inplace(num) + self.assertEqual(m, 0) + self.assertEqual(self.eng.activeQubits, 1) + + @if_has_module + def test_absorb_both_empty(self): + eng2 = pyqrackEngine("Alice", 0) + self.eng.absorb(eng2) + self.assertEqual(self.eng.activeQubits, 0) + self.assertEqual(len(self.eng.qubitReg), 0) + + @if_has_module + def test_absorb_other_empty(self): + num = self.eng.add_fresh_qubit() + self.eng.apply_H(num) + eng2 = pyqrackEngine("Alice", 0) + self.eng.absorb(eng2) + self.assertEqual(self.eng.activeQubits, 1) + self.assertEqual(len(self.eng.qubitReg), 1) + self.eng.apply_Z(num) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_absorb_this_empty_H(self): + eng2 = pyqrackEngine("Alice", 0) + num = eng2.add_fresh_qubit() + eng2.apply_H(num) + self.eng.absorb(eng2) + self.assertEqual(self.eng.activeQubits, 1) + self.assertEqual(len(self.eng.qubitReg), 1) + self.eng.apply_Z(num) + self.eng.apply_H(num) + results = self.eng.engine.measure_shots([0], 10) + self.assertEqual(results, [1] * 10) + + @if_has_module + def test_absorb_this_empty_CNOT(self): + eng2 = pyqrackEngine("Alice", 0) + num1 = eng2.add_fresh_qubit() + num2 = eng2.add_fresh_qubit() + eng2.apply_X(num1) + eng2.apply_CNOT(num1, num2) + self.eng.absorb(eng2) + self.assertEqual(self.eng.activeQubits, 2) + self.assertEqual(len(self.eng.qubitReg), 2) + results = self.eng.engine.measure_shots([num1, num2], 10) + self.assertEqual(results, [3] * 10) + + @if_has_module + def test_absorb_2GHZ(self): + n = 5 + eng2 = pyqrackEngine("Alice", 0) + for eng in [self.eng, eng2]: + qubits = [eng.add_fresh_qubit() for _ in range(n)] + eng.apply_H(qubits[0]) + for i in range(1, n): + eng.apply_CNOT(qubits[0], qubits[i]) + self.eng.absorb(eng2) + self.assertEqual(self.eng.activeQubits, 2 * n) + self.assertEqual(len(self.eng.qubitReg), 2 * n) + + @if_has_module + def test_absorb_to_big_this_empty(self): + eng2 = pyqrackEngine("Alice", 0, 11) + for _ in range(11): + eng2.add_fresh_qubit() + with self.assertRaises(quantumError): + self.eng.absorb(eng2) + + @if_has_module + def test_absorb_to_big(self): + self.eng.add_fresh_qubit() + eng2 = pyqrackEngine("Alice", 0) + for _ in range(10): + eng2.add_fresh_qubit() + with self.assertRaises(quantumError): + self.eng.absorb(eng2) + + @if_has_module + def test_absorb_parts(self): + self.eng.add_fresh_qubit() + eng2 = pyqrackEngine("Alice", 0) + eng2.add_fresh_qubit() + with self.assertRaises(NotImplementedError): + self.eng.absorb_parts([], [], eng2.activeQubits) + + +if __name__ == "__main__": + if _has_module: + unittest.main()