From 6ef266f59c42b301bd16b6acdf23a396788bb4bf Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Thu, 18 Apr 2024 12:35:47 +0100 Subject: [PATCH 01/12] Adding task to checklist --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 558b0333..be02bf20 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,6 +8,7 @@ Please mention any github issues addressed by this PR. # Checklist +- [ ] I have run the tests on a device with GPUs. - [ ] I have performed a self-review of my code. - [ ] I have commented hard-to-understand parts of my code. - [ ] I have made corresponding changes to the public API documentation. From b471f76cfcd934741855b4cb4cf558b96dc6e3c6 Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:27:29 +0100 Subject: [PATCH 02/12] Feature/relabel qubits (#102) * Adding apply_qubit_relabelling to MPS and TTN. Co-authored-by: PabloAndresCQ --- docs/changelog.rst | 5 +++ .../cutensornet/structured_state/general.py | 18 ++++++++++ .../cutensornet/structured_state/mps.py | 27 +++++++++++++++ .../structured_state/simulation.py | 4 +++ .../cutensornet/structured_state/ttn.py | 27 +++++++++++++++ tests/conftest.py | 34 +++++++++++++++++-- tests/test_structured_state.py | 14 ++++++++ 7 files changed, 127 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 72b62b8b..e4bb4c3d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ~~~~~~~~~ +Unreleased +---------- + +* New feature: ``apply_qubit_relabelling`` both for ``MPS`` and ``TTN`` to change the name of their qubits. This is now used within ``simulate`` to take into account the action of implicit SWAPs in pytket circuits (no additional SWAP gates are applied). + 0.6.1 (April 2024) ------------------ diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index 7194b088..429adeb4 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -240,6 +240,24 @@ def apply_scalar(self, scalar: complex) -> StructuredState: """ raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") + @abstractmethod + def apply_qubit_relabelling(self, qubit_map: dict[Qubit, Qubit]) -> StructuredState: + """Relabels each qubit ``q`` as ``qubit_map[q]``. + + This does not apply any SWAP gate, nor it changes the internal structure of the + state. It simply changes the label of the physical bonds of the tensor network. + + Args: + qubit_map: Dictionary mapping each qubit to its new label. + + Returns: + ``self``, to allow for method chaining. + + Raises: + ValueError: If any of the keys in ``qubit_map`` are not qubits in the state. + """ + raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") + @abstractmethod def vdot(self, other: StructuredState) -> complex: """Obtain the inner product of the two states: ````. diff --git a/pytket/extensions/cutensornet/structured_state/mps.py b/pytket/extensions/cutensornet/structured_state/mps.py index 83329f25..1490994b 100644 --- a/pytket/extensions/cutensornet/structured_state/mps.py +++ b/pytket/extensions/cutensornet/structured_state/mps.py @@ -217,6 +217,33 @@ def apply_scalar(self, scalar: complex) -> MPS: self.tensors[0] *= scalar return self + def apply_qubit_relabelling(self, qubit_map: dict[Qubit, Qubit]) -> MPS: + """Relabels each qubit ``q`` as ``qubit_map[q]``. + + This does not apply any SWAP gate, nor it changes the internal structure of the + state. It simply changes the label of the physical bonds of the tensor network. + + Args: + qubit_map: Dictionary mapping each qubit to its new label. + + Returns: + ``self``, to allow for method chaining. + + Raises: + ValueError: If any of the keys in ``qubit_map`` are not qubits in the state. + """ + new_qubit_position = dict() + for q_orig, q_new in qubit_map.items(): + # Check the qubit is in the state + if q_orig not in self.qubit_position: + raise ValueError(f"Qubit {q_orig} is not in the state.") + # Apply the relabelling for this qubit + new_qubit_position[q_new] = self.qubit_position[q_orig] + + self.qubit_position = new_qubit_position + self._logger.debug(f"Relabelled qubits... {qubit_map}") + return self + def canonicalise(self, l_pos: int, r_pos: int) -> None: """Canonicalises the MPS object. diff --git a/pytket/extensions/cutensornet/structured_state/simulation.py b/pytket/extensions/cutensornet/structured_state/simulation.py index 14d0768a..0d68880e 100644 --- a/pytket/extensions/cutensornet/structured_state/simulation.py +++ b/pytket/extensions/cutensornet/structured_state/simulation.py @@ -122,6 +122,9 @@ def simulate( # Apply the circuit's phase to the state state.apply_scalar(np.exp(1j * np.pi * circuit.phase)) + # Relabel qubits according to the implicit swaps (if any) + state.apply_qubit_relabelling(circuit.implicit_qubit_permutation()) + logger.info("Simulation completed.") logger.info(f"Final StructuredState size={state.get_byte_size() / 2**20} MiB") logger.info(f"Final StructuredState fidelity={state.fidelity}") @@ -138,6 +141,7 @@ def prepare_circuit_mps(circuit: Circuit) -> tuple[Circuit, dict[Qubit, Qubit]]: The qubits in the output circuit will be renamed. Implicit SWAPs may be added to the circuit, meaning that the logical qubit held at the ``node[i]`` qubit at the beginning of the circuit may differ from the one it holds at the end. + Consider applying ``apply_qubit_relabelling`` on the MPS after simulation. Args: circuit: The circuit to be simulated. diff --git a/pytket/extensions/cutensornet/structured_state/ttn.py b/pytket/extensions/cutensornet/structured_state/ttn.py index 47e9770f..b15fff79 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn.py +++ b/pytket/extensions/cutensornet/structured_state/ttn.py @@ -289,6 +289,33 @@ def apply_scalar(self, scalar: complex) -> TTN: self.nodes[()].tensor *= scalar return self + def apply_qubit_relabelling(self, qubit_map: dict[Qubit, Qubit]) -> TTN: + """Relabels each qubit ``q`` as ``qubit_map[q]``. + + This does not apply any SWAP gate, nor it changes the internal structure of the + state. It simply changes the label of the physical bonds of the tensor network. + + Args: + qubit_map: Dictionary mapping each qubit to its new label. + + Returns: + ``self``, to allow for method chaining. + + Raises: + ValueError: If any of the keys in ``qubit_map`` are not qubits in the state. + """ + new_qubit_position = dict() + for q_orig, q_new in qubit_map.items(): + # Check the qubit is in the state + if q_orig not in self.qubit_position: + raise ValueError(f"Qubit {q_orig} is not in the state.") + # Apply the relabelling for this qubit + new_qubit_position[q_new] = self.qubit_position[q_orig] + + self.qubit_position = new_qubit_position + self._logger.debug(f"Relabelled qubits... {qubit_map}") + return self + def canonicalise( self, center: Union[RootPath, Qubit], unsafe: bool = False ) -> Tensor: diff --git a/tests/conftest.py b/tests/conftest.py index 69e6d961..a90aedf6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ import pytest import numpy as np from scipy.stats import unitary_group # type: ignore -from pytket.circuit import Circuit, OpType, Unitary2qBox -from pytket.passes import DecomposeBoxes +from pytket.circuit import Circuit, OpType, Unitary2qBox, ToffoliBox, Qubit +from pytket.passes import DecomposeBoxes, CnXPairwiseDecomposition +from pytket.transform import Transform def random_line_circuit(n_qubits: int, layers: int) -> Circuit: @@ -253,3 +254,32 @@ def q8_qvol() -> Circuit: def q15_qvol() -> Circuit: np.random.seed(1) return quantum_volume_circuit(n_qubits=15) + + +@pytest.fixture +def q3_toffoli_box_with_implicit_swaps() -> Circuit: + # Using specific permutation here + perm = { + (False, False): (True, True), + (False, True): (False, False), + (True, False): (True, False), + (True, True): (False, True), + } + + # Create a circuit with more qubits and multiple applications of the permutation + # above + circ = Circuit(3) + + # Create the circuit + circ.add_toffolibox(ToffoliBox(perm), [Qubit(0), Qubit(1)]) # type: ignore + circ.add_toffolibox(ToffoliBox(perm), [Qubit(1), Qubit(2)]) # type: ignore + + DecomposeBoxes().apply(circ) + CnXPairwiseDecomposition().apply(circ) + Transform.OptimiseCliffords().apply(circ) + + # Check that, indeed, there are implicit swaps + implicit_perm = circ.implicit_qubit_permutation() + assert any(qin != qout for qin, qout in implicit_perm.items()) + + return circ diff --git a/tests/test_structured_state.py b/tests/test_structured_state.py index f38f45f0..86e9ffcd 100644 --- a/tests/test_structured_state.py +++ b/tests/test_structured_state.py @@ -262,6 +262,7 @@ def test_canonicalise_ttn(center: Union[RootPath, Qubit]) -> None: pytest.lazy_fixture("q2_lcu3"), # type: ignore pytest.lazy_fixture("q3_v0cx02"), # type: ignore pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q4_with_creates"), # type: ignore pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore @@ -324,6 +325,7 @@ def test_exact_circ_sim(circuit: Circuit, algorithm: SimulationAlgorithm) -> Non pytest.lazy_fixture("q2_lcu3"), # type: ignore pytest.lazy_fixture("q3_v0cx02"), # type: ignore pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q4_with_creates"), # type: ignore pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore @@ -372,6 +374,7 @@ def test_approx_circ_sim_gate_fid( pytest.lazy_fixture("q2_lcu3"), # type: ignore pytest.lazy_fixture("q3_v0cx02"), # type: ignore pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q4_with_creates"), # type: ignore pytest.lazy_fixture("q5_h0s1rz2ry3tk4tk13"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore @@ -586,6 +589,7 @@ def test_postselect_2q_circ(circuit: Circuit, postselect_dict: dict) -> None: "circuit", [ pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore ], ) @@ -607,7 +611,11 @@ def test_postselect_circ(circuit: Circuit, postselect_dict: dict) -> None: with CuTensorNetHandle() as libhandle: cfg = Config() + + circuit, qubit_map = prepare_circuit_mps(circuit) mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, cfg) + mps.apply_qubit_relabelling(qubit_map) + prob = mps.postselect(postselect_dict) assert np.isclose(prob, sv_prob, atol=cfg._atol) assert np.allclose(mps.get_statevector(), sv, atol=cfg._atol) @@ -626,6 +634,7 @@ def test_postselect_circ(circuit: Circuit, postselect_dict: dict) -> None: pytest.lazy_fixture("q2_lcu2"), # type: ignore pytest.lazy_fixture("q2_lcu3"), # type: ignore pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q4_with_creates"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore ], @@ -654,7 +663,9 @@ def test_expectation_value(circuit: Circuit, observable: QubitPauliString) -> No # Simulate the circuit and obtain the expectation value with CuTensorNetHandle() as libhandle: cfg = Config() + circuit, qubit_map = prepare_circuit_mps(circuit) mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, cfg) + mps.apply_qubit_relabelling(qubit_map) assert np.isclose( mps.expectation_value(observable), expectation_value, atol=cfg._atol ) @@ -701,6 +712,7 @@ def test_sample_circ_2q(circuit: Circuit) -> None: "circuit", [ pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q3_toffoli_box_with_implicit_swaps"), # type: ignore pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore ], ) @@ -711,7 +723,9 @@ def test_measure_circ(circuit: Circuit) -> None: qB = circuit.qubits[-3] # Third list significant qubit with CuTensorNetHandle() as libhandle: + circuit, qubit_map = prepare_circuit_mps(circuit) mps = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, Config()) + mps.apply_qubit_relabelling(qubit_map) # Compute the probabilities of each outcome p = {(0, 0): 0.0, (0, 1): 0.0, (1, 0): 0.0, (1, 1): 0.0} From 23fd2edfdaf4eb59e16277dbecb8b8b96ffc112a Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Wed, 8 May 2024 12:22:12 +0100 Subject: [PATCH 03/12] Refactor: apply_matrix to any StructuredState and destroy CuTensorNetHandle (#104) * Refactored code to provide apply_unitary * Adding an explicit destroy method for CuTensorNetHandle --- docs/changelog.rst | 1 + docs/modules/structured_state.rst | 4 +- .../cutensornet/structured_state/general.py | 42 ++++++++- .../cutensornet/structured_state/mps.py | 90 ++++++++++++++----- .../cutensornet/structured_state/mps_gate.py | 39 ++++---- .../cutensornet/structured_state/mps_mpo.py | 46 ++++------ .../cutensornet/structured_state/ttn.py | 71 ++++++++++++--- .../cutensornet/structured_state/ttn_gate.py | 32 +++---- 8 files changed, 212 insertions(+), 113 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e4bb4c3d..120b2dba 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Unreleased ---------- +* New feature: ``apply_unitary`` both for ``MPS`` and ``TTN`` to apply an arbitrary unitary matrix, rather than a ``pytket.Command``. * New feature: ``apply_qubit_relabelling`` both for ``MPS`` and ``TTN`` to change the name of their qubits. This is now used within ``simulate`` to take into account the action of implicit SWAPs in pytket circuits (no additional SWAP gates are applied). 0.6.1 (April 2024) diff --git a/docs/modules/structured_state.rst b/docs/modules/structured_state.rst index 326a43ce..5274b6b1 100644 --- a/docs/modules/structured_state.rst +++ b/docs/modules/structured_state.rst @@ -18,15 +18,17 @@ Simulation .. autoclass:: pytket.extensions.cutensornet.structured_state.CuTensorNetHandle + .. automethod:: destroy + Classes ~~~~~~~ .. autoclass:: pytket.extensions.cutensornet.structured_state.StructuredState() - .. automethod:: __init__ .. automethod:: is_valid .. automethod:: apply_gate + .. automethod:: apply_unitary .. automethod:: apply_scalar .. automethod:: vdot .. automethod:: sample diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index 429adeb4..b6ccf3bb 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -64,12 +64,20 @@ def __init__(self, device_id: Optional[int] = None): self.handle = cutn.create() + def destroy(self) -> None: + """Destroys the memory handle, releasing memory. + + Only call this method if you are initialising a ``CuTensorNetHandle`` outside + a ``with CuTensorNetHandle() as libhandle`` statement. + """ + cutn.destroy(self.handle) + self._is_destroyed = True + def __enter__(self) -> CuTensorNetHandle: return self def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: - cutn.destroy(self.handle) - self._is_destroyed = True + self.destroy() class Config: @@ -224,7 +232,35 @@ def apply_gate(self, gate: Command) -> StructuredState: Raises: RuntimeError: If the ``CuTensorNetHandle`` is out of scope. - RuntimeError: If gate is not supported. + ValueError: If the command introduced is not a unitary gate. + ValueError: If gate acts on more than 2 qubits. + """ + raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") + + @abstractmethod + def apply_unitary( + self, unitary: cp.ndarray, qubits: list[Qubit] + ) -> StructuredState: + """Applies the unitary to the specified qubits of the StructuredState. + + Note: + It is assumed that the matrix provided by the user is unitary. If this is + not the case, the program will still run, but its behaviour is undefined. + + Args: + unitary: The matrix to be applied as a CuPy ndarray. It should either be + a 2x2 matrix if acting on one qubit or a 4x4 matrix if acting on two. + qubits: The qubits the unitary acts on. Only one qubit and two qubit + unitaries are supported. + + Returns: + ``self``, to allow for method chaining. + + Raises: + RuntimeError: If the ``CuTensorNetHandle`` is out of scope. + ValueError: If the number of qubits provided is not one or two. + ValueError: If the size of the matrix does not match with the number of + qubits provided. """ raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") diff --git a/pytket/extensions/cutensornet/structured_state/mps.py b/pytket/extensions/cutensornet/structured_state/mps.py index 1490994b..2ab01469 100644 --- a/pytket/extensions/cutensornet/structured_state/mps.py +++ b/pytket/extensions/cutensornet/structured_state/mps.py @@ -160,9 +160,54 @@ def apply_gate(self, gate: Command) -> MPS: Raises: RuntimeError: If the ``CuTensorNetHandle`` is out of scope. - RuntimeError: If gate acts on more than 2 qubits or acts on non-adjacent + ValueError: If the command introduced is not a unitary gate. + ValueError: If gate acts on more than 2 qubits or acts on non-adjacent qubits. - RuntimeError: If physical bond dimension where gate is applied is not 2. + """ + try: + unitary = gate.op.get_unitary() + except: + raise ValueError("The command introduced is not unitary.") + + # Load the gate's unitary to the GPU memory + unitary = unitary.astype(dtype=self._cfg._complex_t, copy=False) + unitary = cp.asarray(unitary, dtype=self._cfg._complex_t) + + self._logger.debug(f"Applying gate {gate}.") + if len(gate.qubits) not in [1, 2]: + raise ValueError( + "Gates must act on only 1 or 2 qubits! " + + f"This is not satisfied by {gate}." + ) + + self.apply_unitary(unitary, gate.qubits) + + return self + + def apply_unitary( + self, unitary: cp.ndarray, qubits: list[Qubit] + ) -> StructuredState: + """Applies the unitary to the specified qubits of the StructuredState. + + Note: + It is assumed that the matrix provided by the user is unitary. If this is + not the case, the program will still run, but its behaviour is undefined. + + Args: + unitary: The matrix to be applied as a CuPy ndarray. It should either be + a 2x2 matrix if acting on one qubit or a 4x4 matrix if acting on two. + qubits: The qubits the unitary acts on. Only one qubit and two qubit + unitaries are supported. + + Returns: + ``self``, to allow for method chaining. + + Raises: + RuntimeError: If the ``CuTensorNetHandle`` is out of scope. + ValueError: If the number of qubits provided is not one or two. + ValueError: If the size of the matrix does not match with the number of + qubits provided. + ValueError: If qubits are non-adjacent. """ if self._lib._is_destroyed: raise RuntimeError( @@ -170,38 +215,37 @@ def apply_gate(self, gate: Command) -> MPS: "See the documentation of update_libhandle and CuTensorNetHandle.", ) - positions = [self.qubit_position[q] for q in gate.qubits] - if any(self.get_physical_dimension(pos) != 2 for pos in positions): - raise RuntimeError( - "Gates can only be applied to tensors with physical" - + " bond dimension of 2." - ) - self._logger.debug(f"Applying gate {gate}") + self._logger.debug(f"Applying unitary {unitary} on {qubits}.") + + if len(qubits) == 1: + if unitary.shape != (2, 2): + raise ValueError( + "The unitary introduced acts on one qubit but it is not 2x2." + ) - if len(positions) == 1: - self._apply_1q_gate(positions[0], gate.op) + self._apply_1q_unitary(unitary, qubits[0]) # NOTE: if the tensor was in canonical form, it remains being so, # since it is guaranteed that the gate is unitary. - elif len(positions) == 2: + elif len(qubits) == 2: + if unitary.shape != (4, 4): + raise ValueError( + "The unitary introduced acts on two qubits but it is not 4x4." + ) + + positions = [self.qubit_position[q] for q in qubits] dist = positions[1] - positions[0] # We explicitly allow both dist==1 or dist==-1 so that non-symmetric # gates such as CX can use the same Op for the two ways it can be in. if dist not in [1, -1]: - raise RuntimeError( - "Gates must be applied to adjacent qubits! " - + f"This is not satisfied by {gate}." - ) - self._apply_2q_gate((positions[0], positions[1]), gate.op) + raise ValueError("Gates must be applied to adjacent qubits!") + self._apply_2q_unitary(unitary, qubits[0], qubits[1]) # The tensors will in general no longer be in canonical form. self.canonical_form[positions[0]] = None self.canonical_form[positions[1]] = None else: - raise RuntimeError( - "Gates must act on only 1 or 2 qubits! " - + f"This is not satisfied by {gate}." - ) + raise ValueError("Gates must act on only 1 or 2 qubits!") return self @@ -919,13 +963,13 @@ def __len__(self) -> int: """ return len(self.tensors) - def _apply_1q_gate(self, position: int, gate: Op) -> MPS: + def _apply_1q_unitary(self, unitary: cp.ndarray, qubit: Qubit) -> MPS: raise NotImplementedError( "MPS is a base class with no contraction algorithm implemented." + " You must use a subclass of MPS, such as MPSxGate or MPSxMPO." ) - def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPS: + def _apply_2q_unitary(self, unitary: cp.ndarray, q0: Qubit, q1: Qubit) -> MPS: raise NotImplementedError( "MPS is a base class with no contraction algorithm implemented." + " You must use a subclass of MPS, such as MPSxGate or MPSxMPO." diff --git a/pytket/extensions/cutensornet/structured_state/mps_gate.py b/pytket/extensions/cutensornet/structured_state/mps_gate.py index 8c57f868..7dccf4a0 100644 --- a/pytket/extensions/cutensornet/structured_state/mps_gate.py +++ b/pytket/extensions/cutensornet/structured_state/mps_gate.py @@ -25,7 +25,7 @@ except ImportError: warnings.warn("local settings failed to import cutensornet", ImportWarning) -from pytket.circuit import Op +from pytket.circuit import Qubit from .mps import MPS @@ -35,23 +35,19 @@ class MPSxGate(MPS): https://arxiv.org/abs/2002.07730 """ - def _apply_1q_gate(self, position: int, gate: Op) -> MPSxGate: - """Applies the 1-qubit gate to the MPS. + def _apply_1q_unitary(self, unitary: cp.ndarray, qubit: Qubit) -> MPSxGate: + """Applies the 1-qubit unitary to the MPS. This does not increase the dimension of any bond. Args: - position: The position of the MPS tensor that this gate - is applied to. - gate: The gate to be applied. + unitary: The unitary to be applied. + qubit: The qubit the unitary acts on. Returns: ``self``, to allow for method chaining. """ - - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) + position = self.qubit_position[qubit] # Glossary of bond IDs # p -> physical bond of the MPS tensor @@ -66,7 +62,7 @@ def _apply_1q_gate(self, position: int, gate: Op) -> MPSxGate: # Contract new_tensor = cq.contract( gate_bonds + "," + T_bonds + "->" + result_bonds, - gate_tensor, + unitary, self.tensors[position], options={"handle": self._lib.handle, "device_id": self._lib.device_id}, optimize={"path": [(0, 1)]}, @@ -76,23 +72,22 @@ def _apply_1q_gate(self, position: int, gate: Op) -> MPSxGate: self.tensors[position] = new_tensor return self - def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: - """Applies the 2-qubit gate to the MPS. + def _apply_2q_unitary(self, unitary: cp.ndarray, q0: Qubit, q1: Qubit) -> MPSxGate: + """Applies the 2-qubit unitary to the MPS. - If doing so increases the virtual bond dimension beyond ``chi``; - truncation is automatically applied. - The MPS is converted to canonical form before truncating. + The MPS is converted to canonical and truncation is applied if necessary. Args: - positions: The position of the MPS tensors that this gate - is applied to. They must be contiguous. - gate: The gate to be applied. + unitary: The unitary to be applied. + q0: The first qubit in the tuple |q0>|q1> the unitary acts on. + q1: The second qubit in the tuple |q0>|q1> the unitary acts on. Returns: ``self``, to allow for method chaining. """ options = {"handle": self._lib.handle, "device_id": self._lib.device_id} + positions = [self.qubit_position[q0], self.qubit_position[q1]] l_pos = min(positions) r_pos = max(positions) @@ -106,12 +101,8 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: self.get_virtual_dimensions(r_pos)[1], ) - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) - # Reshape into a rank-4 tensor - gate_tensor = cp.reshape(gate_tensor, (2, 2, 2, 2)) + gate_tensor = cp.reshape(unitary, (2, 2, 2, 2)) # Glossary of bond IDs # l -> physical bond of the left tensor in the MPS diff --git a/pytket/extensions/cutensornet/structured_state/mps_mpo.py b/pytket/extensions/cutensornet/structured_state/mps_mpo.py index 192d2566..ad64b64a 100644 --- a/pytket/extensions/cutensornet/structured_state/mps_mpo.py +++ b/pytket/extensions/cutensornet/structured_state/mps_mpo.py @@ -28,7 +28,7 @@ except ImportError: warnings.warn("local settings failed to import cutensornet", ImportWarning) -from pytket.circuit import Op, Qubit +from pytket.circuit import Qubit from .general import CuTensorNetHandle, Tensor, Config from .mps import ( DirMPS, @@ -99,26 +99,22 @@ def update_libhandle(self, libhandle: CuTensorNetHandle) -> None: super().update_libhandle(libhandle) self._aux_mps.update_libhandle(libhandle) - def _apply_1q_gate(self, position: int, gate: Op) -> MPSxMPO: - """Applies the 1-qubit gate to the MPS. + def _apply_1q_unitary(self, unitary: cp.ndarray, qubit: Qubit) -> MPSxMPO: + """Applies the 1-qubit unitary to the MPS. This does not increase the dimension of any bond. Args: - position: The position of the MPS tensor that this gate - is applied to. - gate: The gate to be applied. + unitary: The unitary to be applied. + qubit: The qubit the unitary acts on. Returns: ``self``, to allow for method chaining. """ + position = self.qubit_position[qubit] # Apply the gate to the MPS with eager approximation - self._aux_mps._apply_1q_gate(position, gate) - - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) + self._aux_mps._apply_1q_unitary(unitary, qubit) # Glossary of bond IDs # i -> input to the MPO tensor @@ -140,7 +136,7 @@ def _apply_1q_gate(self, position: int, gate: Op) -> MPSxMPO: # Contract new_tensor = cq.contract( "go," + last_bonds + "->" + new_bonds, - gate_tensor, + unitary, last_tensor, options={"handle": self._lib.handle, "device_id": self._lib.device_id}, optimize={"path": [(0, 1)]}, @@ -154,21 +150,22 @@ def _apply_1q_gate(self, position: int, gate: Op) -> MPSxMPO: return self - def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxMPO: - """Applies the 2-qubit gate to the MPS. + def _apply_2q_unitary(self, unitary: cp.ndarray, q0: Qubit, q1: Qubit) -> MPSxMPO: + """Applies the 2-qubit unitary to the MPS. - If doing so increases the virtual bond dimension beyond ``chi``; - truncation is automatically applied. - The MPS is converted to canonical form before truncating. + The MPS is converted to canonical and truncation is applied if necessary. Args: - positions: The position of the MPS tensors that this gate - is applied to. They must be contiguous. - gate: The gate to be applied. + unitary: The unitary to be applied. + q0: The first qubit in the tuple |q0>|q1> the unitary acts on. + q1: The second qubit in the tuple |q0>|q1> the unitary acts on. Returns: ``self``, to allow for method chaining. """ + options = {"handle": self._lib.handle, "device_id": self._lib.device_id} + + positions = [self.qubit_position[q0], self.qubit_position[q1]] l_pos = min(positions) r_pos = max(positions) @@ -177,14 +174,10 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxMPO: self._flush() # Apply the gate to the MPS with eager approximation - self._aux_mps._apply_2q_gate(positions, gate) - - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) + self._aux_mps._apply_2q_unitary(unitary, q0, q1) # Reshape into a rank-4 tensor - gate_tensor = cp.reshape(gate_tensor, (2, 2, 2, 2)) + gate_tensor = cp.reshape(unitary, (2, 2, 2, 2)) # Glossary of bond IDs # l -> gate's left input bond @@ -200,7 +193,6 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxMPO: gate_bonds = "RLrl" # Apply a QR decomposition on the gate_tensor to shape it as an MPO - options = {"handle": self._lib.handle, "device_id": self._lib.device_id} L, R = tensor.decompose( gate_bonds + "->lsL,rsR", gate_tensor, diff --git a/pytket/extensions/cutensornet/structured_state/ttn.py b/pytket/extensions/cutensornet/structured_state/ttn.py index b15fff79..b28f4c8b 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn.py +++ b/pytket/extensions/cutensornet/structured_state/ttn.py @@ -29,7 +29,7 @@ except ImportError: warnings.warn("local settings failed to import cutensornet", ImportWarning) -from pytket.circuit import Command, Op, Qubit +from pytket.circuit import Command, Qubit from pytket.pauli import QubitPauliString from pytket.extensions.cutensornet.general import set_logger @@ -252,7 +252,46 @@ def apply_gate(self, gate: Command) -> TTN: Raises: RuntimeError: If the ``CuTensorNetHandle`` is out of scope. - RuntimeError: If gate acts on more than 2 qubits. + ValueError: If the command introduced is not a unitary gate. + ValueError: If gate acts on more than 2 qubits. + """ + try: + unitary = gate.op.get_unitary() + except: + raise ValueError("The command introduced is not unitary.") + + # Load the gate's unitary to the GPU memory + unitary = unitary.astype(dtype=self._cfg._complex_t, copy=False) + unitary = cp.asarray(unitary, dtype=self._cfg._complex_t) + + self._logger.debug(f"Applying gate {gate}.") + self.apply_unitary(unitary, gate.qubits) + + return self + + def apply_unitary( + self, unitary: cp.ndarray, qubits: list[Qubit] + ) -> StructuredState: + """Applies the unitary to the specified qubits of the StructuredState. + + Note: + It is assumed that the matrix provided by the user is unitary. If this is + not the case, the program will still run, but its behaviour is undefined. + + Args: + unitary: The matrix to be applied as a CuPy ndarray. It should either be + a 2x2 matrix if acting on one qubit or a 4x4 matrix if acting on two. + qubits: The qubits the unitary acts on. Only one qubit and two qubit + unitaries are supported. + + Returns: + ``self``, to allow for method chaining. + + Raises: + RuntimeError: If the ``CuTensorNetHandle`` is out of scope. + ValueError: If the number of qubits provided is not one or two. + ValueError: If the size of the matrix does not match with the number of + qubits provided. """ if self._lib._is_destroyed: raise RuntimeError( @@ -260,20 +299,24 @@ def apply_gate(self, gate: Command) -> TTN: "See the documentation of update_libhandle and CuTensorNetHandle.", ) - self._logger.debug(f"Applying gate {gate}") + self._logger.debug(f"Applying unitary {unitary} on {qubits}.") - if len(gate.qubits) == 1: - self._apply_1q_gate(gate.qubits[0], gate.op) + if len(qubits) == 1: + if unitary.shape != (2, 2): + raise ValueError( + "The unitary introduced acts on one qubit but it is not 2x2." + ) + self._apply_1q_unitary(unitary, qubits[0]) - elif len(gate.qubits) == 2: - self._apply_2q_gate(gate.qubits[0], gate.qubits[1], gate.op) + elif len(qubits) == 2: + if unitary.shape != (4, 4): + raise ValueError( + "The unitary introduced acts on two qubits but it is not 4x4." + ) + self._apply_2q_unitary(unitary, qubits[0], qubits[1]) else: - # NOTE: This could be supported if gate acts on same group of qubits - raise RuntimeError( - "Gates must act on only 1 or 2 qubits! " - + f"This is not satisfied by {gate}." - ) + raise ValueError("Gates must act on only 1 or 2 qubits!") return self @@ -855,13 +898,13 @@ def copy(self) -> TTN: ) return new_ttn - def _apply_1q_gate(self, qubit: Qubit, gate: Op) -> TTN: + def _apply_1q_unitary(self, unitary: cp.ndarray, qubit: Qubit) -> TTN: raise NotImplementedError( "TTN is a base class with no contraction algorithm implemented." + " You must use a subclass of TTN, such as TTNxGate." ) - def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTN: + def _apply_2q_unitary(self, unitary: cp.ndarray, q0: Qubit, q1: Qubit) -> TTN: raise NotImplementedError( "TTN is a base class with no contraction algorithm implemented." + " You must use a subclass of TTN, such as TTNxGate." diff --git a/pytket/extensions/cutensornet/structured_state/ttn_gate.py b/pytket/extensions/cutensornet/structured_state/ttn_gate.py index d5183939..4bd18639 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn_gate.py +++ b/pytket/extensions/cutensornet/structured_state/ttn_gate.py @@ -25,7 +25,7 @@ except ImportError: warnings.warn("local settings failed to import cutensornet", ImportWarning) -from pytket.circuit import Op, Qubit +from pytket.circuit import Qubit from .ttn import TTN, DirTTN, RootPath @@ -34,23 +34,18 @@ class TTNxGate(TTN): of a circuit as a ``TTN``. """ - def _apply_1q_gate(self, qubit: Qubit, gate: Op) -> TTNxGate: + def _apply_1q_unitary(self, unitary: cp.ndarray, qubit: Qubit) -> TTNxGate: """Applies the 1-qubit gate to the TTN. This does not increase the dimension of any bond. Args: - qubit: The qubit that this gate is applied to. - gate: The gate to be applied. + unitary: The unitary to be applied. + qubit: The qubit the unitary acts on. Returns: ``self``, to allow for method chaining. """ - - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) - path, target = self.qubit_position[qubit] node_tensor = self.nodes[path].tensor n_qbonds = ( @@ -72,7 +67,7 @@ def _apply_1q_gate(self, qubit: Qubit, gate: Op) -> TTNxGate: new_tensor = cq.contract( node_tensor, node_bonds, - gate_tensor, + unitary, ["o", "i"], result_bonds, options={"handle": self._lib.handle, "device_id": self._lib.device_id}, @@ -84,28 +79,23 @@ def _apply_1q_gate(self, qubit: Qubit, gate: Op) -> TTNxGate: self.nodes[path].tensor = new_tensor return self - def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: + def _apply_2q_unitary(self, unitary: cp.ndarray, q0: Qubit, q1: Qubit) -> TTNxGate: """Applies the 2-qubit gate to the TTN. - Truncation is automatically applied according to the parameters - in the ``Config`` object passed to this ``TTN``. - The TTN is converted to canonical form before truncating. + The TTN is converted to canonical and truncation is applied if necessary. Args: - q0: The 0-th qubit the gate acts upon. - q1: The 1-st qubit the gate acts upon. - gate: The gate to be applied. + unitary: The unitary to be applied. + q0: The first qubit in the tuple |q0>|q1> the unitary acts on. + q1: The second qubit in the tuple |q0>|q1> the unitary acts on. Returns: ``self``, to allow for method chaining. """ options = {"handle": self._lib.handle, "device_id": self._lib.device_id} - # Load the gate's unitary to the GPU memory - gate_unitary = gate.get_unitary().astype(dtype=self._cfg._complex_t, copy=False) - gate_tensor = cp.asarray(gate_unitary, dtype=self._cfg._complex_t) # Reshape into a rank-4 tensor - gate_tensor = cp.reshape(gate_tensor, (2, 2, 2, 2)) + gate_tensor = cp.reshape(unitary, (2, 2, 2, 2)) (path_q0, bond_q0) = self.qubit_position[q0] (path_q1, bond_q1) = self.qubit_position[q1] From 454fa011991b0b17687288a81703811cdc027fa6 Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Fri, 10 May 2024 13:43:14 +0100 Subject: [PATCH 04/12] Feature: MPS add_qubit and non-destructive measurements (#105) * Added an add_qubit method to MPS * Added an option for non-destructive measurement. --- docs/changelog.rst | 2 + docs/modules/structured_state.rst | 2 + .../cutensornet/structured_state/general.py | 13 +-- .../cutensornet/structured_state/mps.py | 89 ++++++++++++++++-- .../cutensornet/structured_state/ttn.py | 13 +-- tests/test_structured_state.py | 91 +++++++++++++++++++ 6 files changed, 191 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 120b2dba..752ddc03 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog Unreleased ---------- +* New feature: ``add_qubit`` to add fresh qubits at specified positions in an ``MPS``. +* New feature: added an option to ``measure`` to toggle destructive measurement on/off. Currently only supported for ``MPS``. * New feature: ``apply_unitary`` both for ``MPS`` and ``TTN`` to apply an arbitrary unitary matrix, rather than a ``pytket.Command``. * New feature: ``apply_qubit_relabelling`` both for ``MPS`` and ``TTN`` to change the name of their qubits. This is now used within ``simulate`` to take into account the action of implicit SWAPs in pytket circuits (no additional SWAP gates are applied). diff --git a/docs/modules/structured_state.rst b/docs/modules/structured_state.rst index 5274b6b1..f360d196 100644 --- a/docs/modules/structured_state.rst +++ b/docs/modules/structured_state.rst @@ -51,10 +51,12 @@ Classes .. autoclass:: pytket.extensions.cutensornet.structured_state.MPSxGate() .. automethod:: __init__ + .. automethod:: add_qubit .. autoclass:: pytket.extensions.cutensornet.structured_state.MPSxMPO() .. automethod:: __init__ + .. automethod:: add_qubit Miscellaneous diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index b6ccf3bb..59674790 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -330,17 +330,18 @@ def sample(self) -> dict[Qubit, int]: raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") @abstractmethod - def measure(self, qubits: set[Qubit]) -> dict[Qubit, int]: - """Applies a Z measurement on ``qubits``, updates the state and returns outcome. + def measure(self, qubits: set[Qubit], destructive: bool = True) -> dict[Qubit, int]: + """Applies a Z measurement on each of the ``qubits``. Notes: - After applying this function, ``self`` will contain the projected - state over the non-measured qubits. - - The resulting state has been normalised. + After applying this function, ``self`` will contain the normalised + projected state. Args: qubits: The subset of qubits to be measured. + destructive: If ``True``, the resulting state will not contain the + measured qubits. If ``False``, these qubits will appear on the + state corresponding to the measurement outcome. Defaults to ``True``. Returns: A dictionary mapping the given ``qubits`` to their measurement outcome, diff --git a/pytket/extensions/cutensornet/structured_state/mps.py b/pytket/extensions/cutensornet/structured_state/mps.py index 2ab01469..f852db52 100644 --- a/pytket/extensions/cutensornet/structured_state/mps.py +++ b/pytket/extensions/cutensornet/structured_state/mps.py @@ -288,6 +288,75 @@ def apply_qubit_relabelling(self, qubit_map: dict[Qubit, Qubit]) -> MPS: self._logger.debug(f"Relabelled qubits... {qubit_map}") return self + def add_qubit(self, new_qubit: Qubit, position: int, state: int = 0) -> MPS: + """Adds a qubit at the specified position. + + Args: + new_qubit: The identifier of the qubit to be added to the state. + position: The location the new qubit should be inserted at in the MPS. + Qubits on this and later indexed have their position shifted by 1. + state: Choose either ``0`` or ``1`` for the new qubit's state. + Defaults to ``0``. + + Returns: + ``self``, to allow for method chaining. + + Raises: + ValueError: If ``new_qubit`` already exists in the state. + ValueError: If ``position`` is negative or larger than ``len(self)``. + ValueError: If ``state`` is not ``0`` or ``1``. + """ + options = {"handle": self._lib.handle, "device_id": self._lib.device_id} + + if new_qubit in self.qubit_position.keys(): + raise ValueError( + f"Qubit {new_qubit} cannot be added, it already is in the MPS." + ) + if position < 0 or position > len(self): + raise ValueError(f"Index {position} is not a valid position in the MPS.") + if state not in [0, 1]: + raise ValueError( + f"Cannot initialise qubit to state {state}. Only 0 or 1 are supported." + ) + + # Identify the dimension of the virtual bond where the new qubit will appear + if position == len(self): + dim = self.get_virtual_dimensions(len(self) - 1)[1] # Rightmost bond + else: # Otherwise, pick the left bond of the tensor currently in ``position`` + dim = self.get_virtual_dimensions(position)[0] + + # Create the tensor for I \otimes |state> + identity = cp.eye(dim, dtype=self._cfg._complex_t) + qubit_tensor = cp.zeros(2, dtype=self._cfg._complex_t) + qubit_tensor[state] = 1 + # Apply the tensor product + new_tensor = cq.contract( + "lr,p->lrp", + identity, + qubit_tensor, + options=options, + optimize={"path": [(0, 1)]}, + ) + + # Place this ``new_tensor`` in the MPS at ``position``, + # the previous tensors at ``position`` onwards are shifted to the right + orig_mps_len = len(self) # Store it in variable, since this will change + self.tensors.insert(position, new_tensor) + + # Update the dictionary tracking the canonical form + for pos in reversed(range(position, orig_mps_len)): + self.canonical_form[pos + 1] = self.canonical_form[pos] + # The canonical form of the new tensor is both LEFT and RIGHT, just choose one + self.canonical_form[position] = DirMPS.LEFT # type: ignore + + # Finally, update the dictionary tracking the qubit position + for q, pos in self.qubit_position.items(): + if pos >= position: + self.qubit_position[q] += 1 + self.qubit_position[new_qubit] = position + + return self + def canonicalise(self, l_pos: int, r_pos: int) -> None: """Canonicalises the MPS object. @@ -518,24 +587,25 @@ def sample(self) -> dict[Qubit, int]: mps = self.copy() return mps.measure(mps.get_qubits()) - def measure(self, qubits: set[Qubit]) -> dict[Qubit, int]: - """Applies a Z measurement on ``qubits``, updates the MPS and returns outcome. + def measure(self, qubits: set[Qubit], destructive: bool = True) -> dict[Qubit, int]: + """Applies a Z measurement on each of the ``qubits``. Notes: - After applying this function, ``self`` will contain the MPS of the projected - state over the non-measured qubits. - - The resulting state has been normalised. + After applying this function, ``self`` will contain the normalised + projected state. Args: qubits: The subset of qubits to be measured. + destructive: If ``True``, the resulting state will not contain the + measured qubits. If ``False``, these qubits will remain in the + state. Defaults to ``True``. Returns: A dictionary mapping the given ``qubits`` to their measurement outcome, i.e. either ``0`` or ``1``. Raises: - ValueError: If an element in ``qubits`` is not a qubit in the MPS. + ValueError: If an element in ``qubits`` is not a qubit in the state. """ result = dict() @@ -591,6 +661,11 @@ def measure(self, qubits: set[Qubit]) -> dict[Qubit, int]: self._postselect_qubit(position_qubit_map[pos], postselection_tensor) + # If the measurement is not destructive, we must add the qubit back again + if not destructive: + qubit = position_qubit_map[pos] + self.add_qubit(qubit, pos, state=outcome) + return result def postselect(self, qubit_outcomes: dict[Qubit, int]) -> float: diff --git a/pytket/extensions/cutensornet/structured_state/ttn.py b/pytket/extensions/cutensornet/structured_state/ttn.py index b28f4c8b..a19d2f1e 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn.py +++ b/pytket/extensions/cutensornet/structured_state/ttn.py @@ -650,17 +650,18 @@ def sample(self) -> dict[Qubit, int]: """ raise NotImplementedError(f"Method not implemented in {type(self).__name__}.") - def measure(self, qubits: set[Qubit]) -> dict[Qubit, int]: - """Applies a Z measurement on ``qubits``, updates the state and returns outcome. + def measure(self, qubits: set[Qubit], destructive: bool = True) -> dict[Qubit, int]: + """Applies a Z measurement on each of the ``qubits``. Notes: - After applying this function, ``self`` will contain the projected - state over the non-measured qubits. - - The resulting state has been normalised. + After applying this function, ``self`` will contain the normalised + projected state. Args: qubits: The subset of qubits to be measured. + destructive: If ``True``, the resulting state will not contain the + measured qubits. If ``False``, these qubits will appear on the + state corresponding to the measurement outcome. Defaults to ``True``. Returns: A dictionary mapping the given ``qubits`` to their measurement outcome, diff --git a/tests/test_structured_state.py b/tests/test_structured_state.py index 86e9ffcd..faaceeee 100644 --- a/tests/test_structured_state.py +++ b/tests/test_structured_state.py @@ -744,3 +744,94 @@ def test_measure_circ(circuit: Circuit) -> None: # Check sample frequency consistent with theoretical probability for outcome, count in sample_dict.items(): assert np.isclose(count / n_samples, p[outcome], atol=0.1) + + +def test_mps_qubit_addition_and_measure() -> None: + with CuTensorNetHandle() as libhandle: + config = Config() + mps = MPSxGate( + libhandle, + qubits=[Qubit(0), Qubit(1), Qubit(2), Qubit(3)], + config=config, + ) + + x = cp.asarray( + [ + [0, 1], + [1, 0], + ], + dtype=config._complex_t, + ) + cx = cp.asarray( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + ], + dtype=config._complex_t, + ) + + # Apply some gates + mps.apply_unitary(x, [Qubit(1)]) # |0100> + mps.apply_unitary(cx, [Qubit(1), Qubit(2)]) # |0110> + mps.apply_unitary(cx, [Qubit(2), Qubit(3)]) # |0111> + # Add a qubit at the end of the MPS + mps.add_qubit(new_qubit=Qubit(4), position=len(mps)) # |01110> + # Apply some more gates acting on the new qubit + mps.apply_unitary(cx, [Qubit(3), Qubit(4)]) # |01111> + mps.apply_unitary(cx, [Qubit(4), Qubit(3)]) # |01101> + # Add a qubit at position 3 + mps.add_qubit(new_qubit=Qubit(6), position=3) # |011001> + # Apply some more gates acting on the new qubit + mps.apply_unitary(x, [Qubit(6)]) # |011101> + mps.apply_unitary(cx, [Qubit(6), Qubit(2)]) # |010101> + mps.apply_unitary(cx, [Qubit(6), Qubit(3)]) # |010111> + # Add another qubit at the end of the MPS + mps.add_qubit(new_qubit=Qubit(5), position=len(mps), state=1) # |0101111> + # Apply some more gates acting on the new qubit + mps.apply_unitary(cx, [Qubit(4), Qubit(5)]) # |0101110> + + # The resulting state should be |0101110> + sv = np.zeros(2**7) + sv[int("0101110", 2)] = 1 + + # However, since mps.get_statevector will sort qubits in ILO, the bits would + # change position. Instead, we can relabel the qubits. + mps.apply_qubit_relabelling( + {q: Qubit(i) for q, i in mps.qubit_position.items()} + ) + + # Compare the state vectors + assert np.allclose(mps.get_statevector(), sv) + + # Measure some of the qubits destructively + outcomes = mps.measure({Qubit(0), Qubit(2), Qubit(4)}, destructive=True) + # Since the state is |0101110>, the outcomes are deterministic + assert outcomes[Qubit(0)] == 0 + assert outcomes[Qubit(2)] == 0 + assert outcomes[Qubit(4)] == 1 + + # Note that the qubit identifiers have not been updated, + # so the qubits that were measured are no longer in the MPS. + with pytest.raises(ValueError, match="not a qubit in the MPS"): + mps.measure({Qubit(0)}) + + # Measure some of the remaining qubits non-destructively + outcomes = mps.measure({Qubit(1), Qubit(6)}, destructive=False) + assert outcomes[Qubit(1)] == 1 + assert outcomes[Qubit(6)] == 0 + + # The resulting state should be |1110>, verify it + sv = np.zeros(2**4) + sv[int("1110", 2)] = 1 + assert np.allclose(mps.get_statevector(), sv) + + # Apply a few more gates to check it works + mps.apply_unitary(x, [Qubit(1)]) # |0110> + mps.apply_unitary(cx, [Qubit(3), Qubit(5)]) # |0100> + + # The resulting state should be |0100>, verify it + sv = np.zeros(2**4) + sv[int("0100", 2)] = 1 + assert np.allclose(mps.get_statevector(), sv) From 6979eb7e32828aca94843dac19fade80e71587ea Mon Sep 17 00:00:00 2001 From: cqc-melf <70640934+cqc-melf@users.noreply.github.com> Date: Tue, 14 May 2024 17:07:40 +0100 Subject: [PATCH 05/12] update tket email (#109) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f7b29cc..c33137f8 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ name="pytket-cutensornet", version=metadata["__extension_version__"], author="TKET development team", - author_email="tket-support@cambridgequantum.com", + author_email="tket-support@quantinuum.com", python_requires=">=3.10", project_urls={ "Documentation": "https://tket.quantinuum.com/extensions/pytket-cutensornet/index.html", From 58bac8e50776732c339b67f034a2284f1b843fb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 09:52:19 +0100 Subject: [PATCH 06/12] Update pylint requirement in the python-packages group (#110) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. Updates `pylint` to 3.2.0 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.0...v3.2.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production dependency-group: python-packages ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- lint-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint-requirements.txt b/lint-requirements.txt index bd6bd51f..b6359e02 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,2 +1,2 @@ black~=24.4 -pylint~=3.1 \ No newline at end of file +pylint~=3.2 \ No newline at end of file From ff38a8651a0351e6d78051361d8ac0c2eff93748 Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Wed, 15 May 2024 13:42:30 +0100 Subject: [PATCH 07/12] NetworkX requirement down to 2.8.8 (#112) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c33137f8..17fb4fcf 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ license="Apache 2", packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, - install_requires=["pytket ~= 1.27", "networkx ~= 3.0"], + install_requires=["pytket ~= 1.27", "networkx >= 2.8.8"], classifiers=[ "Environment :: Console", "Programming Language :: Python :: 3.10", From 5b62a174c4ea86186ff22068241aee138b9cfe67 Mon Sep 17 00:00:00 2001 From: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> Date: Wed, 15 May 2024 14:11:49 +0100 Subject: [PATCH 08/12] Add workflow to add new issues to TKET project. (#113) --- .github/workflows/issue-to-project.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/issue-to-project.yml diff --git a/.github/workflows/issue-to-project.yml b/.github/workflows/issue-to-project.yml new file mode 100644 index 00000000..5c8ba372 --- /dev/null +++ b/.github/workflows/issue-to-project.yml @@ -0,0 +1,16 @@ +name: Add issues to project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.6.1 + with: + project-url: https://github.com/orgs/CQCL-DEV/projects/19 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From b68fa79d93c7a1d835cb366715d586c8a72a2360 Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Wed, 15 May 2024 18:29:31 +0100 Subject: [PATCH 09/12] Adding milliseconds to the logger (#111) --- pytket/extensions/cutensornet/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/general.py b/pytket/extensions/cutensornet/general.py index 97abf081..f39ed406 100644 --- a/pytket/extensions/cutensornet/general.py +++ b/pytket/extensions/cutensornet/general.py @@ -18,7 +18,7 @@ def set_logger( logger_name: str, level: int = logging.WARNING, - fmt: str = "[%(asctime)s] %(name)s (%(levelname)s) - %(message)s", + fmt: str = "[%(asctime)s.%(msecs)03d] %(name)s (%(levelname)s) - %(message)s", ) -> Logger: """Initialises and configures a logger object. From db57d6b4279254051524d7f12b81c415a384b14a Mon Sep 17 00:00:00 2001 From: Pablo Andres-Martinez <104848389+PabloAndresCQ@users.noreply.github.com> Date: Thu, 16 May 2024 14:56:20 +0100 Subject: [PATCH 10/12] Feature: provide RNG seed for `StructuredState` simulations (#114) * Adding random seed to configuration parameters * Adding a test on seeded sampling behaviour --- docs/changelog.rst | 1 + .../cutensornet/structured_state/general.py | 9 +++++ .../cutensornet/structured_state/mps.py | 24 ++++++++++++-- .../cutensornet/structured_state/ttn.py | 14 ++++++++ tests/test_structured_state.py | 33 +++++++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 752ddc03..11cc6d38 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Unreleased * New feature: ``add_qubit`` to add fresh qubits at specified positions in an ``MPS``. * New feature: added an option to ``measure`` to toggle destructive measurement on/off. Currently only supported for ``MPS``. +* New feature: a seed can now be provided to ``Config`` objects, providing reproducibility across ``StructuredState`` simulations. * New feature: ``apply_unitary`` both for ``MPS`` and ``TTN`` to apply an arbitrary unitary matrix, rather than a ``pytket.Command``. * New feature: ``apply_qubit_relabelling`` both for ``MPS`` and ``TTN`` to change the name of their qubits. This is now used within ``simulate`` to take into account the action of implicit SWAPs in pytket circuits (no additional SWAP gates are applied). diff --git a/pytket/extensions/cutensornet/structured_state/general.py b/pytket/extensions/cutensornet/structured_state/general.py index 59674790..d8de30f1 100644 --- a/pytket/extensions/cutensornet/structured_state/general.py +++ b/pytket/extensions/cutensornet/structured_state/general.py @@ -87,6 +87,7 @@ def __init__( self, chi: Optional[int] = None, truncation_fidelity: Optional[float] = None, + seed: Optional[int] = None, float_precision: Type[Any] = np.float64, value_of_zero: float = 1e-16, leaf_size: int = 8, @@ -110,6 +111,11 @@ def __init__( ``||^2 >= trucantion_fidelity``, where ``|psi>`` and ``|phi>`` are the states before and after truncation (both normalised). If not provided, it will default to its maximum value 1. + seed: Seed for the random number generator. Setting a seed provides + reproducibility across simulations using ``StructuredState``, in the + sense that they will produce the same sequence of measurement outcomes. + Crucially, consecutive samples taken from the same ``StructuredState`` + can still be different from each other. float_precision: The floating point precision used in tensor calculations; choose from ``numpy`` types: ``np.float64`` or ``np.float32``. Complex numbers are represented using two of such @@ -184,6 +190,8 @@ def __init__( UserWarning, ) + self.seed = seed + if leaf_size >= 65: # Imposed to avoid bond ID collisions # More than 20 qubits is already unreasonable for a leaf anyway raise ValueError("Maximum allowed leaf_size is 65.") @@ -199,6 +207,7 @@ def copy(self) -> Config: return Config( chi=self.chi, truncation_fidelity=self.truncation_fidelity, + seed=self.seed, float_precision=self._real_t, # type: ignore value_of_zero=self.zero, leaf_size=self.leaf_size, diff --git a/pytket/extensions/cutensornet/structured_state/mps.py b/pytket/extensions/cutensornet/structured_state/mps.py index f852db52..dda12f34 100644 --- a/pytket/extensions/cutensornet/structured_state/mps.py +++ b/pytket/extensions/cutensornet/structured_state/mps.py @@ -16,7 +16,7 @@ from typing import Union from enum import Enum -from random import random # type: ignore +from random import Random # type: ignore import numpy as np # type: ignore try: @@ -89,6 +89,8 @@ def __init__( self._lib = libhandle self._cfg = config self._logger = set_logger("MPS", level=config.loglevel) + self._rng = Random() + self._rng.seed(self._cfg.seed) self.fidelity = 1.0 n_tensors = len(qubits) @@ -585,7 +587,13 @@ def sample(self) -> dict[Qubit, int]: # modify the algorithm in `measure`. This may be done eventually if `copy` # is shown to be a bottleneck when sampling (which is likely). mps = self.copy() - return mps.measure(mps.get_qubits()) + outcomes = mps.measure(mps.get_qubits()) + # If the user sets a seed for the MPS, we'd like that every copy of the MPS + # produces the same sequence of samples, but samples within a sequence may be + # different from each other. Achieved by updating the state of `self._rng`. + self._rng.setstate(mps._rng.getstate()) + + return outcomes def measure(self, qubits: set[Qubit], destructive: bool = True) -> dict[Qubit, int]: """Applies a Z measurement on each of the ``qubits``. @@ -649,7 +657,7 @@ def measure(self, qubits: set[Qubit], destructive: bool = True) -> dict[Qubit, i ) # Throw a coin to decide measurement outcome - outcome = 0 if prob > random() else 1 + outcome = 0 if prob > self._rng.random() else 1 result[position_qubit_map[pos]] = outcome self._logger.debug(f"Outcome of qubit at {pos} is {outcome}.") @@ -1025,6 +1033,16 @@ def copy(self) -> MPS: new_mps.canonical_form = self.canonical_form.copy() new_mps.qubit_position = self.qubit_position.copy() + # If the user has set a seed, assume that they'd want every copy + # to behave in the same way, so we copy the RNG state + if self._cfg.seed is not None: + # Setting state (rather than just copying the seed) allows for the + # copy to continue from the same point in the sequence of random + # numbers as the original copy + new_mps._rng.setstate(self._rng.getstate()) + # Otherwise, samples will be different between copies, since their + # self._rng will be initialised from system randomnes when seed=None. + self._logger.debug( "Successfully copied an MPS " f"of size {new_mps.get_byte_size() / 2**20} MiB." diff --git a/pytket/extensions/cutensornet/structured_state/ttn.py b/pytket/extensions/cutensornet/structured_state/ttn.py index a19d2f1e..59460190 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn.py +++ b/pytket/extensions/cutensornet/structured_state/ttn.py @@ -16,6 +16,7 @@ from typing import Optional, Union from enum import IntEnum +from random import Random # type: ignore import math # type: ignore import numpy as np # type: ignore @@ -127,6 +128,9 @@ def __init__( self._lib = libhandle self._cfg = config self._logger = set_logger("TTN", level=config.loglevel) + self._rng = Random() + self._rng.seed(self._cfg.seed) + self.fidelity = 1.0 self.nodes: dict[RootPath, TreeNode] = dict() self.qubit_position: dict[Qubit, tuple[RootPath, int]] = dict() @@ -893,6 +897,16 @@ def copy(self) -> TTN: new_ttn.nodes = {path: node.copy() for path, node in self.nodes.items()} new_ttn.qubit_position = self.qubit_position.copy() + # If the user has set a seed, assume that they'd want every copy + # to behave in the same way, so we copy the RNG state + if self._cfg.seed is not None: + # Setting state (rather than just copying the seed) allows for the + # copy to continue from the same point in the sequence of random + # numbers as the original copy + new_ttn._rng.setstate(self._rng.getstate()) + # Otherwise, samples will be different between copies, since their + # self._rng will be initialised from system randomnes when seed=None. + self._logger.debug( "Successfully copied a TTN " f"of size {new_ttn.get_byte_size() / 2**20} MiB." diff --git a/tests/test_structured_state.py b/tests/test_structured_state.py index faaceeee..c01e32f6 100644 --- a/tests/test_structured_state.py +++ b/tests/test_structured_state.py @@ -671,6 +671,39 @@ def test_expectation_value(circuit: Circuit, observable: QubitPauliString) -> No ) +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q2_lcu2"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + pytest.lazy_fixture("q5_line_circ_30_layers"), # type: ignore + ], +) +def test_sample_with_seed(circuit: Circuit) -> None: + n_samples = 10 + config = Config(seed=1234) + + with CuTensorNetHandle() as libhandle: + mps_0 = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config) + mps_1 = simulate(libhandle, circuit, SimulationAlgorithm.MPSxGate, config) + mps_2 = mps_0.copy() + + all_outcomes = [] + for _ in range(n_samples): + # Check that all copies of the MPS result in the same sample + outcomes_0 = mps_0.sample() + outcomes_1 = mps_1.sample() + outcomes_2 = mps_2.sample() + assert outcomes_0 == outcomes_1 and outcomes_0 == outcomes_2 + + all_outcomes.append(outcomes_0) + + # Check that the outcomes change between different samples + assert not all(outcome == outcomes_0 for outcome in all_outcomes) + + @pytest.mark.parametrize( "circuit", [ From c1c1fa3f83b0c1adb71ff874a5a03efef36ab557 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 09:58:03 +0100 Subject: [PATCH 11/12] Bump actions/add-to-project from 0.6.1 to 1.0.1 (#118) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.6.1 to 1.0.1. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.6.1...v1.0.1) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-to-project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-to-project.yml b/.github/workflows/issue-to-project.yml index 5c8ba372..f55a9f06 100644 --- a/.github/workflows/issue-to-project.yml +++ b/.github/workflows/issue-to-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.6.1 + - uses: actions/add-to-project@v1.0.1 with: project-url: https://github.com/orgs/CQCL-DEV/projects/19 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From 8ef8ab45d4dbf941b4a36ab9acabb2644501d602 Mon Sep 17 00:00:00 2001 From: Alec Edgington Date: Thu, 6 Jun 2024 12:28:30 +0100 Subject: [PATCH 12/12] mainonly --- .github/workflows/build_and_test.yml | 3 +-- .github/workflows/check-examples.yml | 1 - .github/workflows/lint.yml | 5 ++--- README.md | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 793507f4..da5c3b11 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,10 +4,9 @@ on: pull_request: branches: - main - - develop push: branches: - - develop + - main - 'wheel/**' - 'runci/**' release: diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml index 4a043c3c..9bc0dec9 100644 --- a/.github/workflows/check-examples.yml +++ b/.github/workflows/check-examples.yml @@ -3,7 +3,6 @@ name: check examples on: pull_request: branches: - - develop - main schedule: # 04:00 every Saturday morning diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 27c568ff..f477192b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,10 +4,9 @@ on: pull_request: branches: - main - - develop push: branches: - - develop + - main - 'wheel/**' - 'runci/**' @@ -31,4 +30,4 @@ jobs: black --check . - name: Run pylint run: | - pylint --recursive=y --ignore=ttn_tutorial.py,mps_tutorial.py */ \ No newline at end of file + pylint --recursive=y --ignore=ttn_tutorial.py,mps_tutorial.py */ diff --git a/README.md b/README.md index 893394ce..52182f09 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ pip install -e . ## Contributing Pull requests are welcome. To make a PR, first fork the repo, make your proposed -changes on the `develop` branch, and open a PR from your fork. If it passes +changes on the `main` branch, and open a PR from your fork. If it passes tests and is accepted after review, it will be merged in. ### Code style