diff --git a/pyproject.toml b/pyproject.toml index 46dc5403..d52d9177 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ Repository = "https://github.com/QCHackers/tqec" Issues = "https://github.com/QCHackers/tqec/issues" [project.optional-dependencies] -test = ["pytest", "mypy"] +test = ["pytest", "mypy", "pytest-cov"] dev = ["sinter", "pymatching", "jupyterlab", "tqec[test]"] all = ["tqec[test, dev]"] diff --git a/src/tqec/_version.py b/src/tqec/_version.py index 429a90f3..cc9c9344 100644 --- a/src/tqec/_version.py +++ b/src/tqec/_version.py @@ -1,3 +1,3 @@ -import importlib +import importlib.metadata __version__ = importlib.metadata.version("tqec") diff --git a/src/tqec/circuit/circuit.py b/src/tqec/circuit/circuit.py index 99ed3dc6..b06520f4 100644 --- a/src/tqec/circuit/circuit.py +++ b/src/tqec/circuit/circuit.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing as ty from copy import deepcopy import cirq @@ -34,15 +35,16 @@ def generate_circuit( we want to implement. plaquettes: description of the computation that should happen at different time-slices of the quantum error correction experiment (or - at least part of it). + at least part of it). If provided as a dictionary, plaquettes should be + 1-indexed (i.e., ``0 not in plaquettes`` should be ``True``). Returns: a cirq.Circuit instance implementing the (part of) quantum error correction experiment represented by the provided inputs. Raises: - TQECException: if the provided plaquettes do not match the expected - number of plaquettes for the given template. + TQECException: if ``len(plaquettes) != template.expected_plaquettes_number`` or + if plaquettes is provided as a dicitonary and ``0 in plaquettes``. """ # Check that the user gave enough plaquettes. if len(plaquettes) != template.expected_plaquettes_number: @@ -50,6 +52,11 @@ def generate_circuit( f"{len(plaquettes)} plaquettes have been provided, but " f"{template.expected_plaquettes_number} were expected." ) + if isinstance(plaquettes, ty.Mapping) and 0 in plaquettes: + raise TQECException( + "If using a dictionary, the input plaquettes parameter should not " + f"contain the entry 0. Found a value ({plaquettes[0]}) at entry 0." + ) # If plaquettes are given as a list, make that a dict to simplify the following operations if isinstance(plaquettes, list): diff --git a/src/tqec/circuit/circuit_test.py b/src/tqec/circuit/circuit_test.py index 63065652..ee70ed6e 100644 --- a/src/tqec/circuit/circuit_test.py +++ b/src/tqec/circuit/circuit_test.py @@ -1,181 +1,55 @@ -"""Example taken from /notebooks/logical_qubit_memory_experiment.ipynb""" - import cirq +import pytest from tqec.circuit.circuit import generate_circuit -from tqec.circuit.operations.operation import make_shift_coords -from tqec.enums import PlaquetteOrientation -from tqec.plaquette.library import ( - MeasurementRoundedPlaquette, - MeasurementSquarePlaquette, - XXMemoryPlaquette, - XXXXMemoryPlaquette, - ZRoundedInitialisationPlaquette, - ZSquareInitialisationPlaquette, - ZZMemoryPlaquette, - ZZZZMemoryPlaquette, -) +from tqec.exceptions import TQECException +from tqec.plaquette.library import ZSquareInitialisationPlaquette from tqec.plaquette.plaquette import Plaquette -from tqec.templates.constructions.qubit import QubitSquareTemplate -from tqec.templates.scale import Dimension, LinearFunction +from tqec.templates.atomic.rectangle import RawRectangleTemplate +from tqec.templates.base import Template -def _normalise_circuit(norm_circuit: cirq.Circuit) -> cirq.Circuit: - ordered_transformers = [ - cirq.drop_empty_moments, - ] - for transformer in ordered_transformers: - norm_circuit = transformer(norm_circuit) - return norm_circuit +@pytest.fixture +def initialisation_plaquette() -> Plaquette: + return ZSquareInitialisationPlaquette() -def _make_repeated_layer(repeat_circuit: cirq.Circuit) -> cirq.Circuit: - # Note: we do not care on which qubit it is applied, but we want a SHIFT_COORDS instruction - # to be inserted somewhere in the repetition loop. It is inserted at the beginning. - any_qubit = next(iter(repeat_circuit.all_qubits()), None) - assert ( - any_qubit is not None - ), "Could not find any qubit in the given Circuit instance." - circuit_to_repeat = cirq.Circuit([make_shift_coords(0, 0, 1)]) + repeat_circuit - repeated_circuit_operation = cirq.CircuitOperation( - circuit_to_repeat.freeze() - ).repeat(9) - return cirq.Circuit([repeated_circuit_operation]) +@pytest.fixture +def one_by_one_template() -> Template: + return RawRectangleTemplate([[0]]) -def _generate_circuit() -> cirq.Circuit: - template = QubitSquareTemplate(Dimension(2, LinearFunction(2))) - plaquettes: list[list[Plaquette]] = [ - [ - ZRoundedInitialisationPlaquette(PlaquetteOrientation.UP), - XXMemoryPlaquette( - PlaquetteOrientation.UP, - [1, 2, 5, 6, 7, 8], - include_detector=False, - is_first_round=True, - ), - XXMemoryPlaquette(PlaquetteOrientation.UP, [1, 2, 5, 6, 7, 8]), - MeasurementRoundedPlaquette( - PlaquetteOrientation.UP, include_detector=False - ), - ], - [ - ZRoundedInitialisationPlaquette(PlaquetteOrientation.LEFT), - ZZMemoryPlaquette( - PlaquetteOrientation.LEFT, [1, 5, 6, 8], is_first_round=True - ), - ZZMemoryPlaquette(PlaquetteOrientation.LEFT, [1, 5, 6, 8]), - MeasurementRoundedPlaquette(PlaquetteOrientation.LEFT), - ], - [ - ZSquareInitialisationPlaquette(), - XXXXMemoryPlaquette( - [1, 2, 3, 4, 5, 6, 7, 8], include_detector=False, is_first_round=True - ), - XXXXMemoryPlaquette([1, 2, 3, 4, 5, 6, 7, 8]), - MeasurementSquarePlaquette(include_detector=False), - ], - [ - ZSquareInitialisationPlaquette(), - ZZZZMemoryPlaquette([1, 3, 4, 5, 6, 8], is_first_round=True), - ZZZZMemoryPlaquette([1, 3, 4, 5, 6, 8]), - MeasurementSquarePlaquette(), - ], - [ - ZRoundedInitialisationPlaquette(PlaquetteOrientation.RIGHT), - ZZMemoryPlaquette( - PlaquetteOrientation.RIGHT, [1, 3, 4, 8], is_first_round=True - ), - ZZMemoryPlaquette(PlaquetteOrientation.RIGHT, [1, 3, 4, 8]), - MeasurementRoundedPlaquette(PlaquetteOrientation.RIGHT), - ], - [ - ZRoundedInitialisationPlaquette(PlaquetteOrientation.DOWN), - XXMemoryPlaquette( - PlaquetteOrientation.DOWN, - [1, 2, 3, 4, 7, 8], - include_detector=False, - is_first_round=True, - ), - XXMemoryPlaquette(PlaquetteOrientation.DOWN, [1, 2, 3, 4, 7, 8]), - MeasurementRoundedPlaquette( - PlaquetteOrientation.DOWN, include_detector=False - ), - ], - ] - layer_modificators = {1: _make_repeated_layer} +def test_generate_initialisation_circuit_list( + initialisation_plaquette: Plaquette, one_by_one_template +): + circuit = generate_circuit(one_by_one_template, [initialisation_plaquette]) + assert circuit == cirq.Circuit( + cirq.R(q.to_grid_qubit()) for q in initialisation_plaquette.qubits + ) - # 4. Actually create the cirq.Circuit instance by concatenating the circuits generated - # for each layers and potentially modified by the modifiers defined above. - circuit = cirq.Circuit() - for layer_index in range(4): - layer_circuit = generate_circuit( - template, - [plaquette_list[layer_index] for plaquette_list in plaquettes], - ) - layer_circuit = _normalise_circuit(layer_circuit) - circuit += layer_modificators.get(layer_index, lambda circ: circ)(layer_circuit) - return circuit +def test_generate_initialisation_circuit_dict( + initialisation_plaquette: Plaquette, one_by_one_template +): + circuit = generate_circuit(one_by_one_template, {1: initialisation_plaquette}) + assert circuit == cirq.Circuit( + cirq.R(q.to_grid_qubit()) for q in initialisation_plaquette.qubits + ) + +def test_generate_initialisation_circuit_dict_0_indexed( + initialisation_plaquette: Plaquette, one_by_one_template +): + with pytest.raises(TQECException): + generate_circuit(one_by_one_template, {0: initialisation_plaquette}) -def test_generate_circuit(): - """Minimal test to check that the circuit is generated correctly - The target qubits are taken from the orginal notebook output. - """ - generated_circuit = _generate_circuit() - generate_qubits = [(q.row, q.col) for q in generated_circuit.all_qubits()] - target_qubits = [ - (6, 2), - (3, 7), - (3, 9), - (1, 1), - (10, 6), - (8, 8), - (1, 5), - (2, 0), - (6, 4), - (2, 2), - (6, 6), - (1, 3), - (7, 1), - (7, 3), - (4, 2), - (1, 7), - (7, 5), - (1, 9), - (7, 7), - (2, 4), - (6, 8), - (9, 1), - (2, 6), - (9, 3), - (9, 7), - (4, 6), - (4, 4), - (7, 9), - (5, 1), - (5, 3), - (2, 8), - (9, 5), - (5, 7), - (8, 2), - (0, 4), - (9, 9), - (3, 1), - (4, 8), - (5, 5), - (3, 3), - (4, 10), - (3, 5), - (10, 2), - (8, 4), - (5, 9), - (8, 6), - (0, 8), - (6, 0), - (8, 10), - ] - generate_qubits.sort() - target_qubits.sort() - assert generate_qubits == target_qubits + +def test_generate_circuit_wrong_number_of_plaquettes( + initialisation_plaquette: Plaquette, one_by_one_template +): + with pytest.raises(TQECException): + generate_circuit( + one_by_one_template, [initialisation_plaquette, initialisation_plaquette] + ) + with pytest.raises(TQECException): + generate_circuit(one_by_one_template, []) diff --git a/src/tqec/circuit/operations/measurement_map_test.py b/src/tqec/circuit/operations/measurement_map_test.py new file mode 100644 index 00000000..5733813a --- /dev/null +++ b/src/tqec/circuit/operations/measurement_map_test.py @@ -0,0 +1,209 @@ +import cirq +import pytest + +from tqec.circuit.operations.measurement_map import CircuitMeasurementMap, flatten +from tqec.exceptions import TQECException + + +@pytest.fixture +def empty_circuit(): + return cirq.Circuit() + + +@pytest.fixture +def flat_circuit(): + qubits = cirq.GridQubit.rect(10, 10) + return cirq.Circuit( + [cirq.H(q) for q in qubits], + [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)], + [cirq.M(q) for q in qubits], + ) + + +@pytest.fixture +def circuit_with_depth1_circuit_operation(): + qubits = cirq.GridQubit.rect(10, 10) + return cirq.Circuit( + [cirq.H(q) for q in qubits], + cirq.CircuitOperation( + cirq.Circuit( + [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)] + ).freeze() + ), + [cirq.M(q) for q in qubits], + ) + + +@pytest.fixture +def circuit_with_depth2_circuit_operation(): + qubits = cirq.GridQubit.rect(10, 10) + return cirq.Circuit( + cirq.CircuitOperation( + cirq.Circuit( + [cirq.H(q) for q in qubits], + cirq.CircuitOperation( + cirq.Circuit( + [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)] + ).freeze() + ), + [cirq.M(q) for q in qubits], + ).freeze() + ) + ) + + +@pytest.fixture +def circuit_with_depth1_repeated_circuit_operation(): + qubits = cirq.GridQubit.rect(10, 10) + return cirq.Circuit( + [cirq.H(q) for q in qubits], + cirq.CircuitOperation( + cirq.Circuit( + [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)] + ).freeze() + ).repeat(10), + [cirq.M(q) for q in qubits], + ) + + +@pytest.fixture +def circuit_with_repeated_measurements(): + qubits = cirq.GridQubit.rect(10, 10) + return cirq.Circuit( + *[cirq.Moment([cirq.M(q) for q in qubits]) for _ in range(10)], + ) + + +def test_flatten_empty_circuit(empty_circuit): + flattened_circuit = flatten(empty_circuit) + assert flattened_circuit == empty_circuit + + +def test_flatten_flat_circuit(flat_circuit): + flattened_circuit = flatten(flat_circuit) + assert flattened_circuit == flat_circuit + + +def test_flatten_depth1_circuit(circuit_with_depth1_circuit_operation, flat_circuit): + flattened_circuit = flatten(circuit_with_depth1_circuit_operation) + assert flattened_circuit == flat_circuit + + +def test_flatten_depth2_circuit(circuit_with_depth2_circuit_operation, flat_circuit): + flattened_circuit = flatten(circuit_with_depth2_circuit_operation) + assert flattened_circuit == flat_circuit + + +def test_flatten_depth1_repeated_circuit( + circuit_with_depth1_repeated_circuit_operation, +): + flattened_circuit = flatten(circuit_with_depth1_repeated_circuit_operation) + assert all( + not isinstance(op, cirq.CircuitOperation) + for op in flattened_circuit.all_operations() + ) + # There should be 500 CX gates + assert ( + sum(op._num_qubits_() == 2 for op in flattened_circuit.all_operations()) == 500 + ) + + +def test_measurement_map_empty_initialisation(empty_circuit): + CircuitMeasurementMap(empty_circuit) + + +def test_measurement_map_flat_initialisation(flat_circuit): + CircuitMeasurementMap(flat_circuit) + + +def test_measurement_map_depth1_initialisation(circuit_with_depth1_circuit_operation): + CircuitMeasurementMap(circuit_with_depth1_circuit_operation) + + +def test_measurement_map_depth2_initialisation(circuit_with_depth2_circuit_operation): + CircuitMeasurementMap(circuit_with_depth2_circuit_operation) + + +def test_measurement_map_depth1_repeated_initialisation( + circuit_with_depth1_repeated_circuit_operation, +): + CircuitMeasurementMap(circuit_with_depth1_repeated_circuit_operation) + + +def test_measurement_map_lot_of_measurements_initialisation( + circuit_with_repeated_measurements, +): + CircuitMeasurementMap(circuit_with_repeated_measurements) + + +def test_measurement_map_get_measurement_relative_offset_raises_on_positive_offset( + empty_circuit, +): + mmap = CircuitMeasurementMap(empty_circuit) + qubit = cirq.GridQubit(0, 0) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(1, qubit, 0) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(1, qubit, 10) + + +def test_measurement_map_get_measurement_relative_offset_raises_on_invalid_moment( + flat_circuit, +): + mmap = CircuitMeasurementMap(flat_circuit) + qubit = cirq.GridQubit(0, 0) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(-10, qubit, -1) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(-1, qubit, -1) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(3, qubit, -1) + with pytest.raises(TQECException): + mmap.get_measurement_relative_offset(10, qubit, -1) + + +@pytest.mark.parametrize( + "circuit_fixture", + [ + "flat_circuit", + "circuit_with_depth1_circuit_operation", + "circuit_with_depth2_circuit_operation", + ], +) +def test_measurement_map_get_measurement_relative_offset(circuit_fixture, request): + circuit = request.getfixturevalue(circuit_fixture) + mmap = CircuitMeasurementMap(circuit) + qubit = cirq.GridQubit(0, 0) + # The only measurement on qubit (0, 0) is at moment 2 + assert mmap.get_measurement_relative_offset(0, qubit, -1) is None + assert mmap.get_measurement_relative_offset(1, qubit, -1) is None + assert mmap.get_measurement_relative_offset(2, qubit, -1) is not None + assert mmap.get_measurement_relative_offset(2, qubit, -2) is None + + +def test_measurement_map_get_measurement_relative_offset_repeat( + circuit_with_depth1_repeated_circuit_operation, +): + mmap = CircuitMeasurementMap(circuit_with_depth1_repeated_circuit_operation) + qubit = cirq.GridQubit(0, 0) + # The only measurement on qubit (0, 0) is at moment 11 + for moment_index in range(11): + assert mmap.get_measurement_relative_offset(moment_index, qubit, -1) is None + assert mmap.get_measurement_relative_offset(11, qubit, -1) is not None + assert mmap.get_measurement_relative_offset(11, qubit, -2) is None + + +def test_measurement_map_get_measurement_relative_offset_lot_of_measurements( + circuit_with_repeated_measurements, +): + mmap = CircuitMeasurementMap(circuit_with_repeated_measurements) + qubit = cirq.GridQubit(0, 0) + # There are measurements from moment 0 to 9 included + for moment_index in range(10): + for backward_index in range(1, moment_index + 1): + assert ( + mmap.get_measurement_relative_offset( + moment_index, qubit, -backward_index + ) + is not None + ) diff --git a/src/tqec/circuit/operations/operation.py b/src/tqec/circuit/operations/operation.py index 0102c983..6fa28388 100644 --- a/src/tqec/circuit/operations/operation.py +++ b/src/tqec/circuit/operations/operation.py @@ -4,6 +4,7 @@ from typing import Any, Sequence import cirq + from tqec.exceptions import TQECException STIM_TAG = "STIM_OPERATION" diff --git a/src/tqec/circuit/operations/operation_test.py b/src/tqec/circuit/operations/operation_test.py new file mode 100644 index 00000000..02ed254c --- /dev/null +++ b/src/tqec/circuit/operations/operation_test.py @@ -0,0 +1,124 @@ +import cirq +import pytest + +from tqec.circuit.operations.operation import ( + Detector, + Observable, + RelativeMeasurementData, + RelativeMeasurementsRecord, + ShiftCoords, + make_detector, + make_observable, + make_shift_coords, +) +from tqec.exceptions import TQECException + +_qubits_examples = [ + cirq.GridQubit(0, 0), + cirq.GridQubit(2, 2), + cirq.GridQubit(-1, 1), + cirq.GridQubit(-10, -3), + cirq.GridQubit(-1, 0), +] + + +def test_empty_shift_coords(): + with pytest.raises( + TQECException, + match="The number of shift coordinates should be between 1 and 16, but got 0.", + ): + make_shift_coords() + + +def test_shift_coords(): + sc_tagged = make_shift_coords(-1, 0, 18, 2**57) + assert isinstance(sc_tagged.untagged, ShiftCoords) + sc_untagged: ShiftCoords = sc_tagged.untagged + assert sc_untagged.shifts == (-1, 0, 18, 2**57) + + +@pytest.mark.parametrize("origin", _qubits_examples) +def test_empty_detector(origin): + detector_tagged = make_detector(origin, [], time_coordinate=0) + assert isinstance(detector_tagged.untagged, Detector) + detector_untagged: Detector = detector_tagged.untagged + assert detector_untagged.origin == origin + assert detector_untagged.coordinates == (origin.row, origin.col, 0) + + +def test_detector_repeated_relative_measurement(): + origin = cirq.GridQubit(0, 0) + relative_measurements: list[tuple[cirq.GridQubit, int]] = [ + (cirq.GridQubit(0, 0), -1), + (cirq.GridQubit(0, 0), -1), + ] + with pytest.raises(TQECException): + # Duplicated relative measurements + make_detector(origin, relative_measurements, time_coordinate=0) + + +def test_detector_with_relative_measurement(): + origin = cirq.GridQubit(0, 0) + relative_measurements: list[tuple[cirq.GridQubit, int]] = [ + (cirq.GridQubit(0, 0), -1), + (cirq.GridQubit(0, 0), -2), + ] + make_detector(origin, relative_measurements, time_coordinate=0) + + +def test_detector_negative_time_coordinate(): + origin = cirq.GridQubit(0, 0) + relative_measurements: list[tuple[cirq.GridQubit, int]] = [ + (cirq.GridQubit(0, 0), -1), + (cirq.GridQubit(0, 0), -2), + ] + with pytest.raises(TQECException): + make_detector(origin, relative_measurements, time_coordinate=-1) + + +@pytest.mark.parametrize("origin", _qubits_examples) +def test_empty_observable(origin): + observable_tagged = make_observable(origin, [], observable_index=0) + assert isinstance(observable_tagged.untagged, Observable) + observable_untagged: Observable = observable_tagged.untagged + assert observable_untagged.origin == origin + assert observable_untagged.index == 0 + + +def test_observable_with_relative_measurement(): + origin = cirq.GridQubit(0, 0) + relative_measurements: list[tuple[cirq.GridQubit, int]] = [ + (cirq.GridQubit(0, 0), -1), + (cirq.GridQubit(0, 0), -2), + ] + make_observable(origin, relative_measurements, observable_index=0) + + +def test_observable_negative_index(): + origin = cirq.GridQubit(0, 0) + relative_measurements: list[tuple[cirq.GridQubit, int]] = [ + (cirq.GridQubit(0, 0), -1), + (cirq.GridQubit(0, 0), -2), + ] + with pytest.raises(TQECException): + make_observable(origin, relative_measurements, observable_index=-1) + + +def test_relative_measure_data_negative_index(): + with pytest.raises(TQECException): + RelativeMeasurementData(cirq.GridQubit(0, 0), 0) + with pytest.raises(TQECException): + RelativeMeasurementData(cirq.GridQubit(0, 0), 1) + + +def test_relative_measurement_record_duplicated_measurement_data(): + origin = cirq.GridQubit(0, 0) + with pytest.raises(TQECException): + RelativeMeasurementsRecord( + origin, + [ + RelativeMeasurementData(origin, -1), + RelativeMeasurementData(origin, -2), + RelativeMeasurementData(origin, -1), + ], + ) diff --git a/src/tqec/circuit/operations/transformer.py b/src/tqec/circuit/operations/transformer.py index 36fc3c3b..2a2261ff 100644 --- a/src/tqec/circuit/operations/transformer.py +++ b/src/tqec/circuit/operations/transformer.py @@ -38,7 +38,7 @@ def _transform_to_stimcirq_compatible_impl( if isinstance(operation.repetitions, int): operation_repetitions = operation.repetitions elif isinstance(operation.repetitions, sympy.Expr): - operation_repetitions = int(operation.repetitions.evalf()) + operation_repetitions = int(operation.repetitions.evalf()) # type: ignore elif isinstance(operation.repetitions, numpy.integer): operation_repetitions = int(operation.repetitions) else: diff --git a/src/tqec/plaquette/plaquette.py b/src/tqec/plaquette/plaquette.py index 813a9885..035f61e9 100644 --- a/src/tqec/plaquette/plaquette.py +++ b/src/tqec/plaquette/plaquette.py @@ -40,7 +40,7 @@ def __init__( plaquette_qubits = {qubit.to_grid_qubit() for qubit in qubits} circuit_qubits = set(circuit.raw_circuit.all_qubits()) if not circuit_qubits.issubset(plaquette_qubits): - wrong_qubits = plaquette_qubits.difference(circuit_qubits) + wrong_qubits = circuit_qubits.difference(plaquette_qubits) raise TQECException( f"The following qubits ({wrong_qubits}) are in the provided circuit " "but not in the list of PlaquetteQubit." @@ -128,7 +128,10 @@ def get_qubits_on_side(side: PlaquetteSide) -> list[PlaquetteQubit]: class RoundedPlaquette(SquarePlaquette): def __init__( - self, circuit: ScheduledCircuit, orientation: PlaquetteOrientation + self, + circuit: ScheduledCircuit, + orientation: PlaquetteOrientation, + add_unused_qubits: bool = False, ) -> None: """Represents a rounded QEC plaquette diff --git a/src/tqec/plaquette/qubit.py b/src/tqec/plaquette/qubit.py index d9d1f6d8..5c228bfa 100644 --- a/src/tqec/plaquette/qubit.py +++ b/src/tqec/plaquette/qubit.py @@ -1,11 +1,10 @@ from dataclasses import dataclass from cirq import GridQubit - from tqec.position import Position -@dataclass +@dataclass(frozen=True) class PlaquetteQubit: """Defines a qubit in the plaquette coordinate system diff --git a/src/tqec/plaquette/qubit_test.py b/src/tqec/plaquette/qubit_test.py new file mode 100644 index 00000000..7380839e --- /dev/null +++ b/src/tqec/plaquette/qubit_test.py @@ -0,0 +1,21 @@ +import cirq + +from tqec.plaquette.qubit import PlaquetteQubit +from tqec.position import Position + + +def test_creation(): + PlaquetteQubit(Position(0, 0)) + PlaquetteQubit(Position(100000, -2398467235)) + PlaquetteQubit(Position(-234897659345, 349578)) + + +def test_to_grid_qubit_origin(): + pq = PlaquetteQubit(Position(0, 0)) + assert pq.to_grid_qubit() == cirq.GridQubit(0, 0) + + +def test_to_grid_qubit_non_origin(): + row, col = 238957462345, -945678 + pq = PlaquetteQubit(Position(col, row)) + assert pq.to_grid_qubit() == cirq.GridQubit(row, col) diff --git a/src/tqec/position.py b/src/tqec/position.py index 81a20da0..e092796d 100644 --- a/src/tqec/position.py +++ b/src/tqec/position.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -@dataclass +@dataclass(frozen=True) class Position: """Simple wrapper around tuple[int, int]. @@ -16,6 +16,10 @@ class Position: x: int y: int + def to_grid_qubit(self) -> tuple[int, int]: + """Returns the position as a tuple following the cirq.GridQubit coordinate system.""" + return (self.y, self.x) + @dataclass class Shape2D: diff --git a/src/tqec/templates/atomic/rectangle_test.py b/src/tqec/templates/atomic/rectangle_test.py index f4c1acfc..15235ce9 100644 --- a/src/tqec/templates/atomic/rectangle_test.py +++ b/src/tqec/templates/atomic/rectangle_test.py @@ -81,6 +81,16 @@ def test_raw_rectangle_init(): RawRectangleTemplate([[0]]) +def test_raw_rectangle_empty_init(): + with pytest.raises(TQECException): + RawRectangleTemplate([[]]) + + +def test_raw_rectangle_wrongly_sized_init(): + with pytest.raises(TQECException): + RawRectangleTemplate([[0, 1], [1]]) + + def test_raw_rectangle_larger_init(): RawRectangleTemplate( [ diff --git a/src/tqec/templates/base.py b/src/tqec/templates/base.py index d09e6d91..206d03b7 100644 --- a/src/tqec/templates/base.py +++ b/src/tqec/templates/base.py @@ -6,6 +6,7 @@ from dataclasses import dataclass import numpy + from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum from tqec.exceptions import TQECException from tqec.position import Displacement, Shape2D @@ -193,3 +194,17 @@ class TemplateWithIndices: template: Template indices: list[int] + + def __post_init__(self): + if self.template.expected_plaquettes_number != len(self.indices): + raise TQECException( + f"Creating a {self.__class__.__name__} instance with the template " + f"{self.template} (that requires {self.template.expected_plaquettes_number} " + f"plaquette indices) and a non-matching number of plaquette indices " + f"{self.indices}." + ) + if any(i < 0 for i in self.indices): + raise TQECException( + "Cannot have negative plaquette indices. Found a negative index " + f"in {self.indices}." + ) diff --git a/src/tqec/templates/base_test.py b/src/tqec/templates/base_test.py new file mode 100644 index 00000000..35d70f5a --- /dev/null +++ b/src/tqec/templates/base_test.py @@ -0,0 +1,31 @@ +import pytest + +from tqec.exceptions import TQECException +from tqec.templates.atomic.square import AlternatingSquareTemplate +from tqec.templates.base import TemplateWithIndices +from tqec.templates.scale import Dimension, LinearFunction + + +@pytest.fixture +def square_template(): + return AlternatingSquareTemplate(Dimension(2, LinearFunction(2))) + + +def test_template_with_indices_creation(square_template): + twi = TemplateWithIndices(square_template, [1, 2]) + assert twi.indices == [1, 2] + assert twi.template == square_template + + +def test_template_with_indices_creation_wrong_number_of_indices(square_template): + with pytest.raises(TQECException): + TemplateWithIndices(square_template, []) + with pytest.raises(TQECException): + TemplateWithIndices(square_template, [1]) + with pytest.raises(TQECException): + TemplateWithIndices(square_template, [2, 4, 1]) + + +def test_template_with_negative_indices_creation(square_template): + with pytest.raises(TQECException): + TemplateWithIndices(square_template, [-1, 0]) diff --git a/src/tqec/templates/composed.py b/src/tqec/templates/composed.py index b581356d..c7bb566f 100644 --- a/src/tqec/templates/composed.py +++ b/src/tqec/templates/composed.py @@ -2,6 +2,7 @@ import networkx as nx import numpy + from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum from tqec.exceptions import TQECException from tqec.position import Displacement, Position, Shape2D @@ -264,8 +265,10 @@ def _compute_ul_absolute_position(self) -> dict[int, Position]: Returns: a mapping between templates indices and their upper-left corner - absolute position. + absolute position. This mapping is empty if ``self.is_empty``. """ + if self.is_empty: + return {} ul_positions: dict[int, Position] = {0: Position(0, 0)} src: int dest: int @@ -479,3 +482,7 @@ def to_dict(self) -> dict[str, ty.Any]: @property def expected_plaquettes_number(self) -> int: return self._maximum_plaquette_mapping_index + + @property + def is_empty(self) -> bool: + return len(self._templates) == 0 diff --git a/src/tqec/templates/composed_test.py b/src/tqec/templates/composed_test.py new file mode 100644 index 00000000..a8f3f57f --- /dev/null +++ b/src/tqec/templates/composed_test.py @@ -0,0 +1,36 @@ +import pytest + +from tqec.templates.atomic import AlternatingSquareTemplate +from tqec.templates.atomic.rectangle import ( + AlternatingRectangleTemplate, + RawRectangleTemplate, +) +from tqec.templates.base import TemplateWithIndices +from tqec.templates.composed import ComposedTemplate +from tqec.templates.scale import Dimension, FixedDimension, LinearFunction + +_DIMENSIONS = [ + Dimension(2, LinearFunction(2, 0)), + Dimension(2, LinearFunction(4, 0)), + FixedDimension(1), +] +_TEMPLATES_AND_INDICES = [ + (AlternatingSquareTemplate(_DIMENSIONS[0]), [1, 2]), + (AlternatingRectangleTemplate(_DIMENSIONS[0], _DIMENSIONS[1]), [198345, 21]), + (AlternatingRectangleTemplate(_DIMENSIONS[0], _DIMENSIONS[2]), [0, 68]), + (RawRectangleTemplate([[0]]), [1]), +] + + +def test_empty(): + template = ComposedTemplate([]) + assert template.expected_plaquettes_number == 0 + assert template.shape.to_numpy_shape() == (0, 0) + + +@pytest.mark.parametrize("atomic_template_and_indices", _TEMPLATES_AND_INDICES) +def test_one_template(atomic_template_and_indices): + atomic_template, indices = atomic_template_and_indices + template = ComposedTemplate([TemplateWithIndices(atomic_template, indices)]) + assert template.shape == atomic_template.shape + assert template.expected_plaquettes_number == max(indices) diff --git a/src/tqec/templates/shifted.py b/src/tqec/templates/shifted.py new file mode 100644 index 00000000..dbf1438d --- /dev/null +++ b/src/tqec/templates/shifted.py @@ -0,0 +1,66 @@ +import typing as ty +from dataclasses import dataclass + +import numpy + +from tqec.position import Shape2D +from tqec.templates.base import Template +from tqec.templates.scale import Dimension + + +@dataclass +class ScalableOffset: + x: Dimension + y: Dimension + + def scale_to(self, k: int) -> None: + self.x.scale_to(k) + self.y.scale_to(k) + + def to_dict(self) -> dict[str, ty.Any]: + return {"x": self.x.to_dict(), "y": self.y.to_dict()} + + +class ShiftedTemplate(Template): + def __init__( + self, + template: Template, + offset: ScalableOffset, + default_x_increment: int = 2, + default_y_increment: int = 2, + ) -> None: + super().__init__(default_x_increment, default_y_increment) + self._shifted_template = template + self._offset = offset + + def scale_to(self, k: int) -> "ShiftedTemplate": + self._shifted_template.scale_to(k) + self._offset.scale_to(k) + return self + + @property + def shape(self) -> Shape2D: + tshape = self._shifted_template.shape + return Shape2D(self._offset.x.value + tshape.x, self._offset.y.value + tshape.y) + + def to_dict(self) -> dict[str, ty.Any]: + return super().to_dict() | { + "shifted": { + "template": self._shifted_template.to_dict(), + "offset": self._offset.to_dict(), + } + } + + @property + def expected_plaquettes_number(self) -> int: + return self._shifted_template.expected_plaquettes_number + + def instantiate(self, plaquette_indices: ty.Sequence[int]) -> numpy.ndarray: + # Do not explicitely check here, the check is forwarded to the + # shifted Template instance. + arr = numpy.zeros(self.shape.to_numpy_shape(), dtype=int) + tshape = self._shifted_template.shape + xoffset, yoffset = self._offset.x.value, self._offset.y.value + tarr = self._shifted_template.instantiate(plaquette_indices) + arr[yoffset : yoffset + tshape.y, xoffset : xoffset + tshape.x] = tarr + return arr diff --git a/src/tqec/templates/shifted_test.py b/src/tqec/templates/shifted_test.py new file mode 100644 index 00000000..d5057145 --- /dev/null +++ b/src/tqec/templates/shifted_test.py @@ -0,0 +1,67 @@ +import numpy +import pytest + +from tqec.position import Shape2D +from tqec.templates.atomic.square import AlternatingSquareTemplate +from tqec.templates.base import Template +from tqec.templates.scale import Dimension, FixedDimension, LinearFunction +from tqec.templates.shifted import ScalableOffset, ShiftedTemplate + + +@pytest.fixture +def default_template(): + return AlternatingSquareTemplate(Dimension(2, LinearFunction(2))) + + +@pytest.fixture +def zero_offset(): + return ScalableOffset(FixedDimension(0), FixedDimension(0)) + + +@pytest.fixture +def scalable_offset(): + return ScalableOffset( + Dimension(2, LinearFunction(2)), Dimension(2, LinearFunction(2)) + ) + + +def test_scalable_offset_creation(): + ScalableOffset(FixedDimension(2), Dimension(2, LinearFunction(2))) + + +def test_scalable_offset_scaling(): + offset = ScalableOffset( + Dimension(2, LinearFunction(3)), Dimension(2, LinearFunction(2)) + ) + assert offset.x.value == 6 + assert offset.y.value == 4 + offset.scale_to(4) + assert offset.x.value == 12 + assert offset.y.value == 8 + + +def test_zero_shifted_template(default_template: Template, zero_offset: ScalableOffset): + template = ShiftedTemplate(default_template, zero_offset) + assert ( + template.expected_plaquettes_number + == default_template.expected_plaquettes_number + ) + indices = list(range(1, template.expected_plaquettes_number + 1)) + numpy.testing.assert_equal( + template.instantiate(indices), default_template.instantiate(indices) + ) + + +def test_shifted_template(default_template: Template, scalable_offset: ScalableOffset): + template = ShiftedTemplate(default_template, scalable_offset) + template.scale_to(1) + assert ( + template.expected_plaquettes_number + == default_template.expected_plaquettes_number + ) + indices = list(range(1, template.expected_plaquettes_number + 1)) + assert template.shape == Shape2D(4, 4) + numpy.testing.assert_equal( + template.instantiate(indices), + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 2, 1]], + ) diff --git a/src/tqec/templates/stack.py b/src/tqec/templates/stack.py new file mode 100644 index 00000000..00cec91c --- /dev/null +++ b/src/tqec/templates/stack.py @@ -0,0 +1,107 @@ +import typing as ty + +import numpy + +from tqec.position import Shape2D +from tqec.templates.base import Template + + +class StackedTemplate(Template): + def __init__( + self, default_x_increment: int = 2, default_y_increment: int = 2 + ) -> None: + """A template composed of templates stacked on top of each others. + + This class implements a naive stack of Template instances. Each Template instance + added to this class will be superposed on top of the previously added templates, + potentially hiding parts of these. + + ## Warning + This class does no effort to simplify the stack of templates. In particular, the + plaquette indices that should be provided to the instanciate method are directly + forwarded to the stacked templates, from bottom to top. If a stacked Template + instance is hidding completly at least one kind of plaquette, this plaquette index + should still be provided. + + ### Example + Stacking the following template + ```text + 1 2 + 2 1 + ``` + on itself will require 4 (FOUR) template indices when calling `instanciate`: + - the first 2 indices being forwarded to the bottom-most Template, + - the last 2 indices being forwarded to the Template on top of it. + + The instanciation of such a stack using + ```py + stack.instanciate(1, 2, 3, 4) + ``` + will return + ```text + 3 4 + 4 3 + ``` + as the last 2 indices (3 and 4) are forwarded to the top-most Template instance + that hides the bottom one. + """ + super().__init__(default_x_increment, default_y_increment) + self._stack: list[Template] = [] + + def push_template_on_top( + self, + template: Template, + ) -> None: + """Place a new template on the top of the stack. + + The new template can be offset by a certain amount, that might be scalable. + + :raises TQECException: if any of the specified offset coordinates is not positive. + """ + self._stack.append(template) + + def pop_template_from_top(self) -> Template: + """Removes the top-most template from the stack.""" + return self._stack.pop() + + def scale_to(self, k: int) -> "StackedTemplate": + for t in self._stack: + t.scale_to(k) + return self + + @property + def shape(self) -> Shape2D: + shapex, shapey = 0, 0 + for template in self._stack: + tshape = template.shape + shapex = max(shapex, tshape.x) + shapey = max(shapey, tshape.y) + return Shape2D(shapex, shapey) + + def to_dict(self) -> dict[str, ty.Any]: + return super().to_dict() | { + "stack": {"templates": [t.to_dict() for t in self._stack]} + } + + @property + def expected_plaquettes_number(self) -> int: + return sum(t.expected_plaquettes_number for t in self._stack) + + def instantiate(self, plaquette_indices: ty.Sequence[int]) -> numpy.ndarray: + arr = numpy.zeros(self.shape.to_numpy_shape(), dtype=int) + first_non_used_plaquette_index: int = 0 + for template in self._stack: + istart = first_non_used_plaquette_index + istop = istart + template.expected_plaquettes_number + indices = [plaquette_indices[i] for i in range(istart, istop)] + first_non_used_plaquette_index = istop + + tarr = template.instantiate(indices) + yshape, xshape = tarr.shape + + # We do not want "0" plaquettes (i.e., "no plaquette" with our convention) to + # stack over and erase non-zero plaquettes. + # To avoid that, we only replace on the non-zeros entries of the stacked over array. + nonzeros = tarr.nonzero() + arr[0:yshape, 0:xshape][nonzeros] = tarr[nonzeros] + return arr diff --git a/src/tqec/templates/stack_test.py b/src/tqec/templates/stack_test.py new file mode 100644 index 00000000..2c7c091e --- /dev/null +++ b/src/tqec/templates/stack_test.py @@ -0,0 +1,153 @@ +import numpy.testing +import pytest + +from tqec.position import Shape2D +from tqec.templates.atomic.square import AlternatingSquareTemplate +from tqec.templates.base import Template +from tqec.templates.scale import Dimension, FixedDimension, LinearFunction +from tqec.templates.shifted import ScalableOffset, ShiftedTemplate +from tqec.templates.stack import StackedTemplate + + +@pytest.fixture +def small_alternating_template(): + return AlternatingSquareTemplate(Dimension(1, LinearFunction(2))) + + +@pytest.fixture +def shifted_small_alternating_template(): + return ShiftedTemplate( + AlternatingSquareTemplate(Dimension(1, LinearFunction(2))), + ScalableOffset(FixedDimension(2), FixedDimension(2)), + ) + + +@pytest.fixture +def larger_alternating_template(): + return AlternatingSquareTemplate(Dimension(1, LinearFunction(4))) + + +def test_stack_template_over_itself(small_alternating_template: Template): + stack = StackedTemplate() + stack.push_template_on_top(small_alternating_template) + stack.push_template_on_top(small_alternating_template) + instantiation = stack.instantiate([1, 2, 3, 4]) + + numpy.testing.assert_equal( + instantiation, small_alternating_template.instantiate([3, 4]) + ) + + +def test_stack_small_alternating_on_top_of_larger_alternating( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(larger_alternating_template) + stack.push_template_on_top(small_alternating_template) + instantiation = stack.instantiate([1, 2, 3, 4]) + + numpy.testing.assert_equal( + instantiation, [[3, 4, 1, 2], [4, 3, 2, 1], [1, 2, 1, 2], [2, 1, 2, 1]] + ) + + +def test_stack_small_alternating_on_top_of_larger_alternating_expected_plaquette_number( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + assert stack.expected_plaquettes_number == 0 + stack.push_template_on_top(larger_alternating_template) + assert ( + stack.expected_plaquettes_number + == larger_alternating_template.expected_plaquettes_number + ) + stack.push_template_on_top(small_alternating_template) + assert ( + stack.expected_plaquettes_number + == larger_alternating_template.expected_plaquettes_number + + small_alternating_template.expected_plaquettes_number + ) + stack.pop_template_from_top() + assert ( + stack.expected_plaquettes_number + == larger_alternating_template.expected_plaquettes_number + ) + + +def test_stack_small_alternating_with_offset_on_top_of_larger_alternating( + shifted_small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(larger_alternating_template) + stack.push_template_on_top(shifted_small_alternating_template) + instantiation = stack.instantiate([1, 2, 3, 4]) + + numpy.testing.assert_equal( + instantiation, + [[1, 2, 1, 2], [2, 1, 2, 1], [1, 2, 3, 4], [2, 1, 4, 3]], + ) + + +def test_stack_small_alternating_below_of_larger_alternating( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(small_alternating_template) + stack.push_template_on_top(larger_alternating_template) + instantiation = stack.instantiate([1, 2, 3, 4]) + + numpy.testing.assert_equal( + instantiation, + larger_alternating_template.instantiate([3, 4]), + ) + + +def test_stack_template_pop( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(larger_alternating_template) + stack.push_template_on_top(small_alternating_template) + stack.pop_template_from_top() + instantiation = stack.instantiate([1, 2]) + + numpy.testing.assert_equal( + instantiation, larger_alternating_template.instantiate([1, 2]) + ) + + +def test_stack_shape( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(small_alternating_template) + assert stack.shape == Shape2D(2, 2) + stack.push_template_on_top(larger_alternating_template) + assert stack.shape == Shape2D(4, 4) + stack.scale_to(2) + assert stack.shape == Shape2D(8, 8) + stack.pop_template_from_top() + assert stack.shape == Shape2D(4, 4) + + +def test_stack_scale_to( + small_alternating_template: Template, larger_alternating_template: Template +): + stack = StackedTemplate() + stack.push_template_on_top(larger_alternating_template) + stack.push_template_on_top(small_alternating_template) + stack.scale_to(2) + instantiation = stack.instantiate([1, 2, 3, 4]) + numpy.testing.assert_equal( + instantiation, + [ + [3, 4, 3, 4, 1, 2, 1, 2], + [4, 3, 4, 3, 2, 1, 2, 1], + [3, 4, 3, 4, 1, 2, 1, 2], + [4, 3, 4, 3, 2, 1, 2, 1], + [1, 2, 1, 2, 1, 2, 1, 2], + [2, 1, 2, 1, 2, 1, 2, 1], + [1, 2, 1, 2, 1, 2, 1, 2], + [2, 1, 2, 1, 2, 1, 2, 1], + ], + )