From 063dd24cb33f51818a2d0fe24b9138e43cbd10c5 Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 09:42:52 -0800 Subject: [PATCH 01/10] Add the option to compile the quantum world to qubits. --- unitary/alpha/quantum_world.py | 137 ++++++++++++++++++++----- unitary/alpha/quantum_world_test.py | 5 +- unitary/alpha/qudit_state_transform.py | 53 ++++++---- 3 files changed, 145 insertions(+), 50 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 9df1de04..f67bbf0b 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -13,11 +13,12 @@ # limitations under the License. import copy import enum -from typing import Dict, List, Optional, Sequence, Union +from typing import Dict, Iterable, List, Optional, Sequence, Union import cirq from unitary.alpha.quantum_object import QuantumObject from unitary.alpha.sparse_vector_simulator import PostSelectOperation, SparseSimulator +from unitary.alpha.qudit_state_transform import qudit_to_qubit_unitary, _num_bits class QuantumWorld: @@ -36,14 +37,15 @@ class QuantumWorld: how to evaluate the quantum game state. If not specified, this defaults to the built-in cirq Simulator. """ - def __init__(self, objects: Optional[List[QuantumObject]] = None, - sampler=cirq.Simulator()): + sampler=cirq.Simulator(), + compile_to_qubits: bool = True): self.clear() self.sampler = sampler self.use_sparse = isinstance(sampler, SparseSimulator) + self.compile_to_qubits = compile_to_qubits if isinstance(objects, QuantumObject): objects = [objects] @@ -60,6 +62,7 @@ def clear(self): self.effect_history = [] self.object_name_dict: Dict[str, QuantumObject] = {} self.ancilla_names = set() + self.compiled_qubits: Dict[cirq.Qid, List[cirq.Qid]] = {} self.post_selection: Dict[QuantumObject, int] = {} def add_object(self, obj: QuantumObject): @@ -70,9 +73,24 @@ def add_object(self, obj: QuantumObject): already been added to the world. """ if obj.name in self.object_name_dict: - raise ValueError("QuantumObject {obj.name} already added to world.") + raise ValueError( + "QuantumObject {obj.name} already added to world.") self.object_name_dict[obj.name] = obj obj.world = self + if self.compile_to_qubits: + qudit_dim = obj.qubit.dimension + if qudit_dim == 2: + self.compiled_qubits[obj.qubit] = [obj.qubit] + else: + compiled_qubits = [ + QuantumObject(f"compiled_{obj.qubit.name}_{qubit_num}", 0) + for qubit_num in range(_num_bits(qudit_dim)) + ] + for qubit in compiled_qubits: + qubit.world = self + self.compiled_qubits[obj.qubit] = [ + qubit.qubit for qubit in compiled_qubits + ] obj.initial_effect() @property @@ -119,14 +137,44 @@ def _append_op(self, op: cirq.Operation): For the sparse simulator post-selections should be as early as possible to cut down the state size. Also X's since they don't increase the size. """ - if not self.use_sparse: - self.circuit.append(op) - return - if isinstance(op, PostSelectOperation) or op.gate is cirq.X: + if not self.use_sparse or isinstance( + op, PostSelectOperation) or op.gate is cirq.X: strategy = cirq.InsertStrategy.EARLIEST else: strategy = cirq.InsertStrategy.NEW + + if self.compile_to_qubits: + qid_shape = cirq.qid_shape(op) + qudit_dim = max(qid_shape) + if qudit_dim > 2: + num_qudits = len(qid_shape) + new_unitary = qudit_to_qubit_unitary( + qudit_dimension=qudit_dim, + num_qudits=num_qudits, + qudit_unitary=cirq.unitary(op)) + num_qubits_per_qudit = _num_bits(qudit_dim) + num_qubits = num_qubits_per_qudit * num_qudits + all_qubits = [] + for qudit in op.qubits: + qubits = self.compiled_qubits[qudit] + if num_qubits_per_qudit > len(qubits): + # Original padding was not enough. This means that one + # of the qubits in the Operation has a higher dimension + # than this qubit. + for qubit_num in range(len(qubits), + num_qubits_per_qudit): + qubit = QuantumObject( + f"compiled_{qudit.name}_{qubit_num}", 0) + self.add_object(qubit) + qubits.append(qubit.qubit) + self.compiled_qubits[qudit] = qubits + all_qubits.extend(qubits) + + op = cirq.MatrixGate(matrix=new_unitary, + qid_shape=(2, ) * + num_qubits).on(*all_qubits) + self.circuit.append(op, strategy=strategy) def add_effect(self, op_list: List[cirq.Operation]): @@ -163,6 +211,35 @@ def _suggest_num_reps(self, sample_size: int) -> int: sample_size = 100 return sample_size + def _interpret_result(self, result: Union[int, Iterable[int]]): + if self.compile_to_qubits: + return cirq.big_endian_bits_to_int(result) + if isinstance(result, Iterable): + return result[0] + return result + + def add_ancilla(self, + namespace: str, + value: Union[enum.Enum, int] = 0) -> QuantumObject: + """Adds an ancilla qudit object with a unique name. + + Args: + namespace: Custom string to be added in the name + value: The value for the ancilla qudit + + Returns: + The added ancilla object. + """ + count = 0 + ancilla_name = f"ancilla_{namespace}_{count}" + while ancilla_name in self.object_name_dict: + count += 1 + ancilla_name = f"ancilla_{namespace}_{count}" + new_obj = QuantumObject(ancilla_name, value) + self.add_object(new_obj) + self.ancilla_names.add(ancilla_name) + return new_obj + def force_measurement(self, obj: QuantumObject, result: Union[enum.Enum, int]) -> str: """Measures a QuantumObject with a defined outcome. @@ -172,17 +249,22 @@ def force_measurement(self, obj: QuantumObject, result: Union[enum.Enum, to be a particular result. A new qubit set to the initial state of the result. """ - count = 0 - ancilla_name = f"ancilla_{obj.name}_{count}" - while ancilla_name in self.object_name_dict: - count += 1 - ancilla_name = f"ancilla_{obj.name}_{count}" - new_obj = QuantumObject(ancilla_name, result) - self.add_object(new_obj) - self.ancilla_names.add(ancilla_name) + new_obj = self.add_ancilla(namespace=obj.name, value=result) + if self.compile_to_qubits and self.compiled_qubits: + obj_qubits = self.compiled_qubits.get(obj.qubit, [obj.qubit]) + new_obj_qubits = self.compiled_qubits.get(new_obj.qubit, + [new_obj.qubit]) + qubit_remapping_dict = dict({ + *zip(obj_qubits, new_obj_qubits), + *zip(new_obj_qubits, obj_qubits) + }) + else: + qubit_remapping_dict = { + obj.qubit: new_obj.qubit, + new_obj.qubit: obj.qubit + } self.circuit = self.circuit.transform_qubits( - lambda q: q if q != obj.qubit and q != new_obj.qubit else - (new_obj.qubit if q == obj.qubit else obj.qubit)) + lambda q: qubit_remapping_dict.get(q, q)) post_selection = result.value if isinstance(result, enum.Enum) else result self.post_selection[new_obj] = post_selection @@ -220,8 +302,10 @@ def peek( if objects is None: objects = self.objects measure_set = set(objects + list(self.post_selection.keys())) - measure_circuit.append( - [cirq.measure(p.qubit, key=p.qubit.name) for p in measure_set]) + measure_circuit.append([ + cirq.measure(self.compiled_qubits.get(p.qubit, [p.qubit]), + key=p.qubit.name) for p in measure_set + ]) results = self.sampler.run(measure_circuit, repetitions=num_reps) # Perform post-selection @@ -229,15 +313,15 @@ def peek( for rep in range(num_reps): post_selected = True for obj in self.post_selection.keys(): - result = results.measurements[obj.name][rep][0] + result = self._interpret_result( + results.measurements[obj.name][rep]) if result != self.post_selection[obj]: post_selected = False break if post_selected: rtn_list.append([ - results.measurements[obj.name][rep] - for obj in objects - if obj.name not in self.ancilla_names + self._interpret_result(results.measurements[obj.name][rep]) + for obj in objects if obj.name not in self.ancilla_names ]) if len(rtn_list) == count: break @@ -294,7 +378,7 @@ def get_histogram(self, histogram.append({state: 0 for state in range(obj.num_states)}) for result in peek_results: for idx in range(len(objects)): - histogram[idx][result[idx][0]] += 1 + histogram[idx][result[idx]] += 1 return histogram def get_probabilities(self, @@ -314,7 +398,8 @@ def get_probabilities(self, probabilities = [] for obj_hist in histogram: probabilities.append({ - state: obj_hist[state] / count for state in range(len(obj_hist)) + state: obj_hist[state] / count + for state in range(len(obj_hist)) }) return probabilities diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index d3795d12..576b7520 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -88,8 +88,9 @@ def test_two_qutrits(): [StopLight.YELLOW, StopLight.GREEN], [StopLight.YELLOW, StopLight.GREEN], ] - expected = "green (d=3): ────X(0_2)───\n\nyellow (d=3): ───X(0_1)───" - assert str(board.circuit) == expected + if not board.compile_to_qubits: + expected = "green (d=3): ────X(0_2)───\n\nyellow (d=3): ───X(0_1)───" + assert str(board.circuit) == expected @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) diff --git a/unitary/alpha/qudit_state_transform.py b/unitary/alpha/qudit_state_transform.py index f9a8d522..5acdba6e 100644 --- a/unitary/alpha/qudit_state_transform.py +++ b/unitary/alpha/qudit_state_transform.py @@ -17,6 +17,14 @@ import numpy as np +def _num_bits(num: int) -> int: + """Returns the minimum number of bits needed to represent the input.""" + result = 1 + while 2**result < num: + result += 1 + return result + + def _nearest_power_of_two_ceiling(qudit_dim: int) -> int: """Returns the smallest power of two greater than or equal to qudit_dim.""" if qudit_dim == 0: @@ -55,14 +63,15 @@ def qudit_to_qubit_state( Expected shape: `((2 ^ m) ^ num_qudits,)`. """ # Reshape the state vector to a `num_qudits` rank tensor. - state_tensor = qudit_state_vector.reshape((qudit_dimension,) * num_qudits) + state_tensor = qudit_state_vector.reshape((qudit_dimension, ) * num_qudits) # Number of extra elements needed in each dimension if represented using qubits. - padding_amount = _nearest_power_of_two_ceiling(qudit_dimension) - qudit_dimension + padding_amount = _nearest_power_of_two_ceiling( + qudit_dimension) - qudit_dimension # Expand the number of elements in each dimension by the padding_amount. Fill # the new elements with the _pad_value. - padded_state_tensor = np.pad( - state_tensor, pad_width=(0, padding_amount), constant_values=_pad_value - ) + padded_state_tensor = np.pad(state_tensor, + pad_width=(0, padding_amount), + constant_values=_pad_value) # Return a flattened state vector view of the final tensor. return np.ravel(padded_state_tensor) @@ -93,9 +102,10 @@ def qubit_to_qudit_state( """ mbit_dimension = _nearest_power_of_two_ceiling(qudit_dimension) # Reshape the state vector to a `num_qudits` rank tensor. - state_tensor = qubit_state_vector.reshape((mbit_dimension,) * num_qudits) + state_tensor = qubit_state_vector.reshape((mbit_dimension, ) * num_qudits) # Shrink the number of elements in each dimension up to the qudit_dimension, ignoring the rest. - trimmed_state_tensor = state_tensor[(slice(qudit_dimension),) * num_qudits] + trimmed_state_tensor = state_tensor[(slice(qudit_dimension), ) * + num_qudits] # Return a flattened state vector view of the final tensor. return np.ravel(trimmed_state_tensor) @@ -131,7 +141,8 @@ def qudit_to_qubit_unitary( A numpy array representing the input unitary using m-qubits-per-qudit. Expected shape: `((2 ^ m) ^ num_qudits, (2 ^ m) ^ num_qudits)`. """ - dim_qubit_space = _nearest_power_of_two_ceiling(qudit_dimension) ** num_qudits + dim_qubit_space = _nearest_power_of_two_ceiling( + qudit_dimension)**num_qudits if memoize: # Perform the transform of the below array from qubit to qudit space so that the indices @@ -150,14 +161,14 @@ def qudit_to_qubit_unitary( iter_range = range(qudit_dimension**num_qudits) for i, j in itertools.product(iter_range, iter_range): # Use the index map to populate the appropriate element in the qubit representation. - result[d_to_b_index_map[i]][d_to_b_index_map[j]] = qudit_unitary[i][j] + result[d_to_b_index_map[i]][ + d_to_b_index_map[j]] = qudit_unitary[i][j] return result # Treat the unitary as a num_qudits^2 system's state vector and represent it using qubits (pad # with 0s). - padded_unitary = qudit_to_qubit_state( - qudit_dimension, num_qudits * 2, np.ravel(qudit_unitary) - ) + padded_unitary = qudit_to_qubit_state(qudit_dimension, num_qudits * 2, + np.ravel(qudit_unitary)) # A qubit-based state vector with the extra padding bits having 1s and rest having 0s. This # vector marks only the bits that are padded. pad_qubits_vector = qudit_to_qubit_state( @@ -169,9 +180,8 @@ def qudit_to_qubit_unitary( # Reshape the padded unitary to the final shape and add a diagonal matrix corresponding to the # pad_qubits_vector. This addition ensures that the invalid states with the "padding" bits map # to identity, preserving unitarity. - return padded_unitary.reshape(dim_qubit_space, dim_qubit_space) + np.diag( - pad_qubits_vector - ) + return padded_unitary.reshape(dim_qubit_space, + dim_qubit_space) + np.diag(pad_qubits_vector) def qubit_to_qudit_unitary( @@ -202,12 +212,11 @@ def qubit_to_qudit_unitary( # Treat unitary as a `num_qudits*2` qudit system state vector. effective_num_qudits = num_qudits * 2 # Reshape the state vector to a `num_qudits*2` rank tensor. - unitary_tensor = qubit_unitary.reshape((mbit_dimension,) * effective_num_qudits) + unitary_tensor = qubit_unitary.reshape( + (mbit_dimension, ) * effective_num_qudits) # Shrink the number of elements in each dimension up to the qudit_dimension, ignoring the rest. - trimmed_unitary_tensor = unitary_tensor[ - (slice(qudit_dimension),) * effective_num_qudits - ] + trimmed_unitary_tensor = unitary_tensor[(slice(qudit_dimension), ) * + effective_num_qudits] # Return a flat unitary view of the final tensor. - return trimmed_unitary_tensor.reshape( - qudit_dimension**num_qudits, qudit_dimension**num_qudits - ) + return trimmed_unitary_tensor.reshape(qudit_dimension**num_qudits, + qudit_dimension**num_qudits) From e4f7a36f683589e9e92d8d5c5368e20beb670929 Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 10:13:47 -0800 Subject: [PATCH 02/10] Unify ancilla and compiled qubits logic. Add tests. --- unitary/alpha/quantum_world.py | 41 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index f67bbf0b..4dbcd0d6 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -82,15 +82,10 @@ def add_object(self, obj: QuantumObject): if qudit_dim == 2: self.compiled_qubits[obj.qubit] = [obj.qubit] else: - compiled_qubits = [ - QuantumObject(f"compiled_{obj.qubit.name}_{qubit_num}", 0) - for qubit_num in range(_num_bits(qudit_dim)) - ] - for qubit in compiled_qubits: - qubit.world = self - self.compiled_qubits[obj.qubit] = [ - qubit.qubit for qubit in compiled_qubits - ] + self.compiled_qubits[obj.qubit] = [] + for qubit_num in range(_num_bits(qudit_dim)): + new_obj = self.add_ancilla(obj.qubit.name) + self.compiled_qubits[obj.qubit].append(new_obj.qubit) obj.initial_effect() @property @@ -164,12 +159,9 @@ def _append_op(self, op: cirq.Operation): # than this qubit. for qubit_num in range(len(qubits), num_qubits_per_qudit): - qubit = QuantumObject( - f"compiled_{qudit.name}_{qubit_num}", 0) - self.add_object(qubit) - qubits.append(qubit.qubit) - self.compiled_qubits[qudit] = qubits - all_qubits.extend(qubits) + new_obj = self.add_ancilla(qudit.name) + qubits.append(new_obj.qubit) + all_qubits.extend(self.compiled_qubits[qudit]) op = cirq.MatrixGate(matrix=new_unitary, qid_shape=(2, ) * @@ -300,10 +292,13 @@ def peek( measure_circuit = self.circuit.copy() if objects is None: - objects = self.objects + objects = [ + obj for obj in self.objects + if obj.name not in self.ancilla_names + ] measure_set = set(objects + list(self.post_selection.keys())) measure_circuit.append([ - cirq.measure(self.compiled_qubits.get(p.qubit, [p.qubit]), + cirq.measure(self.compiled_qubits.get(p.qubit, p.qubit), key=p.qubit.name) for p in measure_set ]) results = self.sampler.run(measure_circuit, repetitions=num_reps) @@ -321,7 +316,7 @@ def peek( if post_selected: rtn_list.append([ self._interpret_result(results.measurements[obj.name][rep]) - for obj in objects if obj.name not in self.ancilla_names + for obj in objects ]) if len(rtn_list) == count: break @@ -348,7 +343,10 @@ def pop( self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection))) if objects is None: - objects = self.objects + objects = [ + obj for obj in self.objects + if obj.name not in self.ancilla_names + ] results = self.peek(objects, convert_to_enum=convert_to_enum) for idx, result in enumerate(results[0]): self.force_measurement(objects[idx], result) @@ -369,7 +367,10 @@ def get_histogram(self, counts for each state of the given object. """ if not objects: - objects = self.objects + objects = [ + obj for obj in self.objects + if obj.name not in self.ancilla_names + ] peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) From 970e424137159b29aacd2882085039e3a2352850 Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 14:38:28 -0800 Subject: [PATCH 03/10] More tests --- unitary/alpha/quantum_effect_test.py | 22 ++-- unitary/alpha/quantum_object_test.py | 24 ++-- unitary/alpha/quantum_world.py | 2 +- unitary/alpha/quantum_world_test.py | 135 +++++++++++++++------- unitary/alpha/qubit_effects_test.py | 48 +++++--- unitary/alpha/qudit_effects_test.py | 13 ++- unitary/examples/tictactoe/tic_tac_toe.py | 63 +++++----- 7 files changed, 194 insertions(+), 113 deletions(-) diff --git a/unitary/alpha/quantum_effect_test.py b/unitary/alpha/quantum_effect_test.py index db2b9d25..b11cb28a 100644 --- a/unitary/alpha/quantum_effect_test.py +++ b/unitary/alpha/quantum_effect_test.py @@ -23,9 +23,11 @@ Q1 = cirq.NamedQubit("q1") +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_quantum_if(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_quantum_if(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 1) piece2 = alpha.QuantumObject("q1", 0) board.add_object(piece) @@ -44,9 +46,11 @@ def test_quantum_if(simulator): assert (result[1] == 1 for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_anti_control(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_anti_control(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 0) piece2 = alpha.QuantumObject("q1", 0) board.add_object(piece) @@ -72,8 +76,9 @@ def test_no_world(): alpha.Flip()(piece) -def test_bad_length(): - board = alpha.QuantumWorld() +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_bad_length(compile_to_qubits): + board = alpha.QuantumWorld(compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 1) board.add_object(piece) with pytest.raises(ValueError, match="Not able to equate"): @@ -83,8 +88,9 @@ def test_bad_length(): alpha.Split()(piece) -def test_no_qutrits(): - board = alpha.QuantumWorld() +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_no_qutrits(compile_to_qubits): + board = alpha.QuantumWorld(compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 2) board.add_object(piece) with pytest.raises(ValueError, match="Cannot apply effect to qids"): diff --git a/unitary/alpha/quantum_object_test.py b/unitary/alpha/quantum_object_test.py index e6b5f070..821b7efc 100644 --- a/unitary/alpha/quantum_object_test.py +++ b/unitary/alpha/quantum_object_test.py @@ -21,10 +21,13 @@ from unitary.alpha.sparse_vector_simulator import SparseSimulator +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_negation(simulator): +def test_negation(simulator, compile_to_qubits): piece = alpha.QuantumObject("t", 0) - board = alpha.QuantumWorld(piece, sampler=simulator()) + board = alpha.QuantumWorld(piece, + sampler=simulator(), + compile_to_qubits=compile_to_qubits) assert board.peek() == [[0]] -piece assert board.peek() == [[1]] @@ -34,17 +37,21 @@ def test_negation(simulator): assert board.peek() == [[1]] +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_add_world_after_state_change(simulator): +def test_add_world_after_state_change(simulator, compile_to_qubits): piece = alpha.QuantumObject("t", 0) piece += 1 - board = alpha.QuantumWorld(piece, sampler=simulator()) + board = alpha.QuantumWorld(piece, + sampler=simulator(), + compile_to_qubits=compile_to_qubits) assert board.peek() == [[1]] -def test_qutrit(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_qutrit(compile_to_qubits): piece = alpha.QuantumObject("t", 2) - board = alpha.QuantumWorld(piece) + board = alpha.QuantumWorld(piece, compile_to_qubits=compile_to_qubits) assert board.peek() == [[2]] piece += 1 assert board.peek() == [[0]] @@ -58,14 +65,15 @@ def test_qutrit(): assert board.peek() == [[2]] -def test_enum(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_enum(compile_to_qubits): class Color(enum.Enum): RED = 0 YELLOW = 1 GREEN = 2 piece = alpha.QuantumObject("t", Color.YELLOW) - board = alpha.QuantumWorld(piece) + board = alpha.QuantumWorld(piece, compile_to_qubits=compile_to_qubits) assert board.peek() == [[Color.YELLOW]] piece += Color.YELLOW assert board.peek() == [[Color.GREEN]] diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 4dbcd0d6..a4cbb5e4 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -40,7 +40,7 @@ class QuantumWorld: def __init__(self, objects: Optional[List[QuantumObject]] = None, sampler=cirq.Simulator(), - compile_to_qubits: bool = True): + compile_to_qubits: bool = False): self.clear() self.sampler = sampler diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index 576b7520..a8e2f28e 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -31,39 +31,49 @@ class StopLight(enum.Enum): GREEN = 2 -def test_duplicate_objects(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_duplicate_objects(compile_to_qubits): light = alpha.QuantumObject("test", Light.GREEN) - board = alpha.QuantumWorld([light]) + board = alpha.QuantumWorld([light], compile_to_qubits=compile_to_qubits) with pytest.raises(ValueError, match="already added"): board.add_object(alpha.QuantumObject("test", Light.RED)) -def test_get_object_by_name(): + +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_get_object_by_name(compile_to_qubits): light = alpha.QuantumObject("test", Light.GREEN) light2 = alpha.QuantumObject("test2", Light.RED) - board = alpha.QuantumWorld([light, light2]) + board = alpha.QuantumWorld([light, light2], + compile_to_qubits=compile_to_qubits) assert board.get_object_by_name("test") == light assert board.get_object_by_name("test2") == light2 assert board.get_object_by_name("test3") == None +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_one_qubit(simulator): +def test_one_qubit(simulator, compile_to_qubits): light = alpha.QuantumObject("test", Light.GREEN) - board = alpha.QuantumWorld([light], sampler=simulator()) + board = alpha.QuantumWorld([light], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) assert board.peek() == [[Light.GREEN]] assert board.peek([light], count=2) == [[Light.GREEN], [Light.GREEN]] light = alpha.QuantumObject("test", 1) - board = alpha.QuantumWorld([light]) + board = alpha.QuantumWorld([light], compile_to_qubits=compile_to_qubits) assert board.peek() == [[1]] assert board.peek([light], count=2) == [[1], [1]] assert board.pop() == [1] +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_two_qubits(simulator): +def test_two_qubits(simulator, compile_to_qubits): light = alpha.QuantumObject("green", Light.GREEN) light2 = alpha.QuantumObject("red", Light.RED) - board = alpha.QuantumWorld([light, light2], sampler=simulator()) + board = alpha.QuantumWorld([light, light2], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) assert board.peek() == [[Light.GREEN, Light.RED]] assert board.peek(convert_to_enum=False) == [[1, 0]] assert board.peek([light], count=2) == [[Light.GREEN], [Light.GREEN]] @@ -75,14 +85,18 @@ def test_two_qubits(simulator): ] -def test_two_qutrits(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_two_qutrits(compile_to_qubits): light = alpha.QuantumObject("yellow", StopLight.YELLOW) light2 = alpha.QuantumObject("green", StopLight.GREEN) - board = alpha.QuantumWorld([light, light2]) + board = alpha.QuantumWorld([light, light2], + compile_to_qubits=compile_to_qubits) assert board.peek(convert_to_enum=False) == [[1, 2]] assert board.peek() == [[StopLight.YELLOW, StopLight.GREEN]] - assert board.peek([light], count=2) == [[StopLight.YELLOW], [StopLight.YELLOW]] - assert board.peek([light2], count=2) == [[StopLight.GREEN], [StopLight.GREEN]] + assert board.peek([light], count=2) == [[StopLight.YELLOW], + [StopLight.YELLOW]] + assert board.peek([light2], count=2) == [[StopLight.GREEN], + [StopLight.GREEN]] assert board.peek(count=3) == [ [StopLight.YELLOW, StopLight.GREEN], [StopLight.YELLOW, StopLight.GREEN], @@ -93,12 +107,15 @@ def test_two_qutrits(): assert str(board.circuit) == expected +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_pop(simulator): +def test_pop(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", Light.GREEN) light2 = alpha.QuantumObject("l2", Light.RED) light3 = alpha.QuantumObject("l3", Light.RED) - board = alpha.QuantumWorld([light, light2, light3], sampler=simulator()) + board = alpha.QuantumWorld([light, light2, light3], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) alpha.Split()(light, light2, light3) results = board.peek([light2, light3], count=200) assert all(result[0] != result[1] for result in results) @@ -111,11 +128,14 @@ def test_pop(simulator): assert all(result[1] != popped for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_pop_and_reuse(simulator): +def test_pop_and_reuse(simulator, compile_to_qubits): """Tests reusing a popped qubit.""" light = alpha.QuantumObject("l1", Light.GREEN) - board = alpha.QuantumWorld([light], sampler=simulator()) + board = alpha.QuantumWorld([light], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) popped = board.pop([light])[0] assert popped == Light.GREEN alpha.Flip()(light) @@ -123,10 +143,13 @@ def test_pop_and_reuse(simulator): assert popped == Light.RED +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_undo(simulator): +def test_undo(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", Light.GREEN) - board = alpha.QuantumWorld([light], sampler=simulator()) + board = alpha.QuantumWorld([light], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) alpha.Flip()(light) results = board.peek([light], count=200) assert all(result[0] == Light.RED for result in results) @@ -135,18 +158,22 @@ def test_undo(simulator): assert all(result[0] == Light.GREEN for result in results) -def test_undo_no_effects(): - board = alpha.QuantumWorld([]) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_undo_no_effects(compile_to_qubits): + board = alpha.QuantumWorld([], compile_to_qubits=compile_to_qubits) with pytest.raises(IndexError, match='No effects'): board.undo_last_effect() +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_undo_post_select(simulator): +def test_undo_post_select(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", Light.GREEN) light2 = alpha.QuantumObject("l2", Light.RED) light3 = alpha.QuantumObject("l3", Light.RED) - board = alpha.QuantumWorld([light, light2, light3], sampler=simulator()) + board = alpha.QuantumWorld([light, light2, light3], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) alpha.Split()(light, light2, light3) # After split, should be fifty-fifty @@ -171,14 +198,17 @@ def test_undo_post_select(simulator): assert not all(result[0] == Light.GREEN for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_pop_not_enough_reps(simulator): +def test_pop_not_enough_reps(simulator, compile_to_qubits): """Tests forcing a measurement of a rare outcome, so that peek needs to be called recursively to get more occurances. """ lights = [alpha.QuantumObject("l" + str(i), Light.RED) for i in range(15)] - board = alpha.QuantumWorld(lights, sampler=simulator()) + board = alpha.QuantumWorld(lights, + sampler=simulator(), + compile_to_qubits=compile_to_qubits) alpha.Flip()(lights[0]) alpha.Split()(lights[0], lights[1], lights[2]) alpha.Split()(lights[2], lights[3], lights[4]) @@ -198,15 +228,19 @@ def test_pop_not_enough_reps(simulator): assert all(result[0] == Light.GREEN for result in results) results = board.peek(count=200) assert all(len(result) == 15 for result in results) - assert all(result == [Light.RED] * 14 + [Light.GREEN] for result in results) + assert all(result == [Light.RED] * 14 + [Light.GREEN] + for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_pop_qubits_twice(simulator): +def test_pop_qubits_twice(simulator, compile_to_qubits): """Tests popping qubits twice, so that 2 ancillas are created for each qubit.""" lights = [alpha.QuantumObject("l" + str(i), Light.RED) for i in range(3)] - board = alpha.QuantumWorld(lights, sampler=simulator()) + board = alpha.QuantumWorld(lights, + sampler=simulator(), + compile_to_qubits=compile_to_qubits) alpha.Flip()(lights[0]) alpha.Split()(lights[0], lights[1], lights[2]) result = board.pop() @@ -223,40 +257,48 @@ def test_pop_qubits_twice(simulator): assert all(peek_result == result2 for peek_result in peek_results) -def test_combine_overlapping_worlds(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_combine_overlapping_worlds(compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) - world1 = alpha.QuantumWorld([l1]) + world1 = alpha.QuantumWorld([l1], compile_to_qubits=compile_to_qubits) l2 = alpha.QuantumObject("l1", StopLight.YELLOW) - world2 = alpha.QuantumWorld([l2]) + world2 = alpha.QuantumWorld([l2], compile_to_qubits=compile_to_qubits) with pytest.raises(ValueError, match="overlapping"): world1.combine_with(world2) with pytest.raises(ValueError, match="overlapping"): world2.combine_with(world1) -def test_combine_incompatibly_sparse_worlds(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_combine_incompatibly_sparse_worlds(compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) - world1 = alpha.QuantumWorld([l1], sampler=cirq.Simulator()) + world1 = alpha.QuantumWorld([l1], + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) l2 = alpha.QuantumObject("l2", StopLight.YELLOW) - world2 = alpha.QuantumWorld([l2], sampler=alpha.SparseSimulator()) + world2 = alpha.QuantumWorld([l2], + sampler=alpha.SparseSimulator(), + compile_to_qubits=compile_to_qubits) with pytest.raises(ValueError, match="sparse"): world1.combine_with(world2) with pytest.raises(ValueError, match="sparse"): world2.combine_with(world1) -def test_combine_worlds(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_combine_worlds(compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) l2 = alpha.QuantumObject("l2", Light.RED) l3 = alpha.QuantumObject("l3", Light.RED) - world1 = alpha.QuantumWorld([l1, l2, l3]) + world1 = alpha.QuantumWorld([l1, l2, l3], + compile_to_qubits=compile_to_qubits) # Split and pop to test post-selection after combining alpha.Split()(l1, l2, l3) result = world1.pop() l4 = alpha.QuantumObject("stop_light", StopLight.YELLOW) - world2 = alpha.QuantumWorld([l4]) + world2 = alpha.QuantumWorld([l4], compile_to_qubits=compile_to_qubits) world2.combine_with(world1) results = world2.peek(count=100) @@ -266,10 +308,14 @@ def test_combine_worlds(): assert all(actual == expected for actual in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) -def test_get_histogram_and_get_probabilities_one_binary_qobject(simulator): +def test_get_histogram_and_get_probabilities_one_binary_qobject( + simulator, compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) - world = alpha.QuantumWorld([l1], sampler=simulator()) + world = alpha.QuantumWorld([l1], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) histogram = world.get_histogram() assert histogram == [{0: 0, 1: 100}] probs = world.get_probabilities() @@ -298,9 +344,11 @@ def test_get_histogram_and_get_probabilities_one_binary_qobject(simulator): assert 0.1 <= bin_probs[0] <= 1.0 -def test_get_histogram_and_get_probabilities_one_trinary_qobject(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_get_histogram_and_get_probabilities_one_trinary_qobject( + compile_to_qubits): l1 = alpha.QuantumObject("l1", StopLight.YELLOW) - world = alpha.QuantumWorld([l1]) + world = alpha.QuantumWorld([l1], compile_to_qubits=compile_to_qubits) histogram = world.get_histogram() assert histogram == [{0: 0, 1: 100, 2: 0}] probs = world.get_probabilities() @@ -309,10 +357,11 @@ def test_get_histogram_and_get_probabilities_one_trinary_qobject(): assert bin_probs == [1.0] -def test_get_histogram_and_get_probabilities_two_qobjects(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_get_histogram_and_get_probabilities_two_qobjects(compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) l2 = alpha.QuantumObject("l2", StopLight.YELLOW) - world = alpha.QuantumWorld([l1, l2]) + world = alpha.QuantumWorld([l1, l2], compile_to_qubits=compile_to_qubits) histogram = world.get_histogram() assert histogram == [{0: 0, 1: 100}, {0: 0, 1: 100, 2: 0}] probs = world.get_probabilities() diff --git a/unitary/alpha/qubit_effects_test.py b/unitary/alpha/qubit_effects_test.py index 1fb13fb3..99027f42 100644 --- a/unitary/alpha/qubit_effects_test.py +++ b/unitary/alpha/qubit_effects_test.py @@ -21,27 +21,33 @@ from unitary.alpha.sparse_vector_simulator import SparseSimulator +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_flip(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_flip(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", 0) board.add_object(piece) alpha.Flip()(piece) assert board.circuit == cirq.Circuit(cirq.X(cirq.NamedQubit("t"))) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_superposition(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_superposition(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", 0) board.add_object(piece) alpha.Superposition()(piece) assert board.circuit == cirq.Circuit(cirq.H(cirq.NamedQubit("t"))) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_move(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_move(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) board.add_object(piece1) @@ -59,9 +65,11 @@ def test_move(simulator): assert all(result == [0, 1] for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_phased_move(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_phased_move(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) board.add_object(piece1) @@ -79,9 +87,11 @@ def test_phased_move(simulator): assert all(result == [0, 1] for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_split(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_split(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) piece3 = alpha.QuantumObject("c", 0) @@ -94,15 +104,17 @@ def test_split(simulator): b = cirq.NamedQubit("b") c = cirq.NamedQubit("c") expected_circuit.append(cirq.X(a)) - expected_circuit.append(cirq.SWAP(a, b) ** 0.5) - expected_circuit.append(cirq.SWAP(a, c) ** 0.5) - expected_circuit.append(cirq.SWAP(a, c) ** 0.5) + expected_circuit.append(cirq.SWAP(a, b)**0.5) + expected_circuit.append(cirq.SWAP(a, c)**0.5) + expected_circuit.append(cirq.SWAP(a, c)**0.5) assert board.circuit == expected_circuit +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) -def test_phased_split(simulator): - board = alpha.QuantumWorld(sampler=simulator()) +def test_phased_split(simulator, compile_to_qubits): + board = alpha.QuantumWorld(sampler=simulator(), + compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) piece3 = alpha.QuantumObject("c", 0) @@ -115,7 +127,7 @@ def test_phased_split(simulator): b = cirq.NamedQubit("b") c = cirq.NamedQubit("c") expected_circuit.append(cirq.X(a)) - expected_circuit.append(cirq.ISWAP(a, b) ** 0.5) - expected_circuit.append(cirq.ISWAP(a, c) ** 0.5) - expected_circuit.append(cirq.ISWAP(a, c) ** 0.5) + expected_circuit.append(cirq.ISWAP(a, b)**0.5) + expected_circuit.append(cirq.ISWAP(a, c)**0.5) + expected_circuit.append(cirq.ISWAP(a, c)**0.5) assert board.circuit == expected_circuit diff --git a/unitary/alpha/qudit_effects_test.py b/unitary/alpha/qudit_effects_test.py index 089ab0a2..2e5c7f34 100644 --- a/unitary/alpha/qudit_effects_test.py +++ b/unitary/alpha/qudit_effects_test.py @@ -13,8 +13,7 @@ # limitations under the License. # import enum - -import cirq +import pytest import unitary.alpha as alpha @@ -25,8 +24,9 @@ class StopLight(enum.Enum): GREEN = 2 -def test_qudit_cycle(): - board = alpha.QuantumWorld() +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_qudit_cycle(compile_to_qubits): + board = alpha.QuantumWorld(compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", StopLight.GREEN) board.add_object(piece) alpha.QuditCycle(3)(piece) @@ -43,8 +43,9 @@ def test_qudit_cycle(): assert all(result == [StopLight.YELLOW] for result in results) -def test_qudit_flip(): - board = alpha.QuantumWorld() +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_qudit_flip(compile_to_qubits): + board = alpha.QuantumWorld(compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", StopLight.GREEN) board.add_object(piece) alpha.QuditFlip(3, 0, 2)(piece) diff --git a/unitary/examples/tictactoe/tic_tac_toe.py b/unitary/examples/tictactoe/tic_tac_toe.py index db22fdfc..e14cab37 100644 --- a/unitary/examples/tictactoe/tic_tac_toe.py +++ b/unitary/examples/tictactoe/tic_tac_toe.py @@ -18,9 +18,12 @@ from unitary.examples.tictactoe.enums import TicTacSquare, TicTacResult, TicTacRules from unitary.examples.tictactoe.tic_tac_split import TicTacSplit - _SQUARE_NAMES = "abcdefghi" -_MARK_SYMBOLS = {TicTacSquare.EMPTY: ".", TicTacSquare.X: "X", TicTacSquare.O: "O"} +_MARK_SYMBOLS = { + TicTacSquare.EMPTY: ".", + TicTacSquare.X: "X", + TicTacSquare.O: "O" +} # Possible ways to get three in a row (rows, columns, and diagonal) indices _POSSIBLE_WINS = [ @@ -35,7 +38,8 @@ ] -def _histogram(results: List[List[TicTacSquare]]) -> List[Dict[TicTacSquare, int]]: +def _histogram( + results: List[List[TicTacSquare]]) -> List[Dict[TicTacSquare, int]]: """Turns a list of whole board measurements into a histogram. Returns: @@ -44,7 +48,11 @@ def _histogram(results: List[List[TicTacSquare]]) -> List[Dict[TicTacSquare, int """ hist = [] for idx in range(9): - hist.append({TicTacSquare.EMPTY: 0, TicTacSquare.X: 0, TicTacSquare.O: 0}) + hist.append({ + TicTacSquare.EMPTY: 0, + TicTacSquare.X: 0, + TicTacSquare.O: 0 + }) for r in results: for idx in range(9): hist[idx][r[idx]] += 1 @@ -100,12 +108,13 @@ class TicTacToe: human-friendly text format or by using `sample()` to get one or more samples (measurements) of the board. """ - - def __init__(self, rules: TicTacRules = TicTacRules.QUANTUM_V3): - self.clear() + def __init__(self, + rules: TicTacRules = TicTacRules.QUANTUM_V3, + run_on_hardware: bool = False): + self.clear(run_on_hardware) self.rules = rules - def clear(self) -> None: + def clear(self, run_on_hardware: bool = False) -> None: """Clears the TicTacToe board. Sets all 9 squares to empty. @@ -116,7 +125,8 @@ def clear(self) -> None: for name in _SQUARE_NAMES: self.empty_squares.add(name) self.squares[name] = QuantumObject(name, TicTacSquare.EMPTY) - self.board = QuantumWorld(list(self.squares.values())) + self.board = QuantumWorld(list(self.squares.values()), + compile_to_qubits=run_on_hardware) def result(self) -> TicTacResult: """Returns the result of the TicTacToe game. @@ -153,13 +163,13 @@ def move(self, move: str, mark: TicTacSquare) -> TicTacResult: if len(move) > 2 or len(move) == 0: raise ValueError(f"Your move ({move}) must be one or two letters.") if not all(m in _SQUARE_NAMES for m in move): - raise ValueError(f"Your move ({move}) can only have these: {_SQUARE_NAMES}") + raise ValueError( + f"Your move ({move}) can only have these: {_SQUARE_NAMES}") if len(move) == 1: # Check if the square is empty if move not in self.empty_squares: raise ValueError( - f"You cannot put your token on non-empty square {move}" - ) + f"You cannot put your token on non-empty square {move}") # Flip the square to the correct value (mark.value) QuditFlip(3, 0, mark.value)(self.squares[move]) # This square is now no longer empty @@ -168,33 +178,26 @@ def move(self, move: str, mark: TicTacSquare) -> TicTacResult: # Check if rules allow quantum moves if self.rules == TicTacRules.CLASSICAL: raise ValueError( - f"Quantum moves are not allowed in a classical TicTacToe" - ) + f"Quantum moves are not allowed in a classical TicTacToe") # Check if either square is non-empty. Splitting on top of # non-empty squares is only allowed at full quantumness - if ( - (move[0] not in self.empty_squares) - or (move[1] not in self.empty_squares) - ) and (self.rules == TicTacRules.QUANTUM_V1): + if ((move[0] not in self.empty_squares) or + (move[1] not in self.empty_squares)) and ( + self.rules == TicTacRules.QUANTUM_V1): raise ValueError( f"This ruleset ({0}) does not allow splits on \ - top of non-empty squares".format( - self.rules - ) - ) + top of non-empty squares".format(self.rules)) # TicTacSplit first flips the first square before performing a split # If either of the two involved squares is empty, we want to do the # split on that square. if move[1] in self.empty_squares: - TicTacSplit(mark, self.rules)( - self.squares[move[1]], self.squares[move[0]] - ) + TicTacSplit(mark, self.rules)(self.squares[move[1]], + self.squares[move[0]]) else: - TicTacSplit(mark, self.rules)( - self.squares[move[0]], self.squares[move[1]] - ) + TicTacSplit(mark, self.rules)(self.squares[move[0]], + self.squares[move[1]]) # The involved squares are now no longer empty self.empty_squares.discard(move[0]) @@ -219,7 +222,9 @@ def sample(self, count: int = 1) -> List[str]: would return '.X.XO..OO'. """ - return [_result_to_str(result) for result in self.board.peek(count=count)] + return [ + _result_to_str(result) for result in self.board.peek(count=count) + ] def print(self) -> str: """Returns the TicTacToe board in ASCII form.""" From b4e2803f1630767ad3843162ff4f28bd9852321d Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 14:47:56 -0800 Subject: [PATCH 04/10] Reformat with black --- unitary/alpha/quantum_effect_test.py | 6 ++-- unitary/alpha/quantum_object_test.py | 12 +++---- unitary/alpha/qubit_effects_test.py | 30 +++++++---------- unitary/alpha/qudit_state_transform.py | 45 +++++++++++++------------- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/unitary/alpha/quantum_effect_test.py b/unitary/alpha/quantum_effect_test.py index b11cb28a..29198acf 100644 --- a/unitary/alpha/quantum_effect_test.py +++ b/unitary/alpha/quantum_effect_test.py @@ -26,8 +26,7 @@ @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_quantum_if(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 1) piece2 = alpha.QuantumObject("q1", 0) board.add_object(piece) @@ -49,8 +48,7 @@ def test_quantum_if(simulator, compile_to_qubits): @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_anti_control(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("q0", 0) piece2 = alpha.QuantumObject("q1", 0) board.add_object(piece) diff --git a/unitary/alpha/quantum_object_test.py b/unitary/alpha/quantum_object_test.py index 821b7efc..59375e70 100644 --- a/unitary/alpha/quantum_object_test.py +++ b/unitary/alpha/quantum_object_test.py @@ -25,9 +25,9 @@ @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_negation(simulator, compile_to_qubits): piece = alpha.QuantumObject("t", 0) - board = alpha.QuantumWorld(piece, - sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld( + piece, sampler=simulator(), compile_to_qubits=compile_to_qubits + ) assert board.peek() == [[0]] -piece assert board.peek() == [[1]] @@ -42,9 +42,9 @@ def test_negation(simulator, compile_to_qubits): def test_add_world_after_state_change(simulator, compile_to_qubits): piece = alpha.QuantumObject("t", 0) piece += 1 - board = alpha.QuantumWorld(piece, - sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld( + piece, sampler=simulator(), compile_to_qubits=compile_to_qubits + ) assert board.peek() == [[1]] diff --git a/unitary/alpha/qubit_effects_test.py b/unitary/alpha/qubit_effects_test.py index 99027f42..6e2a33de 100644 --- a/unitary/alpha/qubit_effects_test.py +++ b/unitary/alpha/qubit_effects_test.py @@ -24,8 +24,7 @@ @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_flip(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", 0) board.add_object(piece) alpha.Flip()(piece) @@ -35,8 +34,7 @@ def test_flip(simulator, compile_to_qubits): @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_superposition(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece = alpha.QuantumObject("t", 0) board.add_object(piece) alpha.Superposition()(piece) @@ -46,8 +44,7 @@ def test_superposition(simulator, compile_to_qubits): @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_move(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) board.add_object(piece1) @@ -68,8 +65,7 @@ def test_move(simulator, compile_to_qubits): @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_phased_move(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) board.add_object(piece1) @@ -90,8 +86,7 @@ def test_phased_move(simulator, compile_to_qubits): @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_split(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) piece3 = alpha.QuantumObject("c", 0) @@ -104,17 +99,16 @@ def test_split(simulator, compile_to_qubits): b = cirq.NamedQubit("b") c = cirq.NamedQubit("c") expected_circuit.append(cirq.X(a)) - expected_circuit.append(cirq.SWAP(a, b)**0.5) - expected_circuit.append(cirq.SWAP(a, c)**0.5) - expected_circuit.append(cirq.SWAP(a, c)**0.5) + expected_circuit.append(cirq.SWAP(a, b) ** 0.5) + expected_circuit.append(cirq.SWAP(a, c) ** 0.5) + expected_circuit.append(cirq.SWAP(a, c) ** 0.5) assert board.circuit == expected_circuit @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, SparseSimulator]) def test_phased_split(simulator, compile_to_qubits): - board = alpha.QuantumWorld(sampler=simulator(), - compile_to_qubits=compile_to_qubits) + board = alpha.QuantumWorld(sampler=simulator(), compile_to_qubits=compile_to_qubits) piece1 = alpha.QuantumObject("a", 1) piece2 = alpha.QuantumObject("b", 0) piece3 = alpha.QuantumObject("c", 0) @@ -127,7 +121,7 @@ def test_phased_split(simulator, compile_to_qubits): b = cirq.NamedQubit("b") c = cirq.NamedQubit("c") expected_circuit.append(cirq.X(a)) - expected_circuit.append(cirq.ISWAP(a, b)**0.5) - expected_circuit.append(cirq.ISWAP(a, c)**0.5) - expected_circuit.append(cirq.ISWAP(a, c)**0.5) + expected_circuit.append(cirq.ISWAP(a, b) ** 0.5) + expected_circuit.append(cirq.ISWAP(a, c) ** 0.5) + expected_circuit.append(cirq.ISWAP(a, c) ** 0.5) assert board.circuit == expected_circuit diff --git a/unitary/alpha/qudit_state_transform.py b/unitary/alpha/qudit_state_transform.py index 5acdba6e..4754452c 100644 --- a/unitary/alpha/qudit_state_transform.py +++ b/unitary/alpha/qudit_state_transform.py @@ -63,15 +63,14 @@ def qudit_to_qubit_state( Expected shape: `((2 ^ m) ^ num_qudits,)`. """ # Reshape the state vector to a `num_qudits` rank tensor. - state_tensor = qudit_state_vector.reshape((qudit_dimension, ) * num_qudits) + state_tensor = qudit_state_vector.reshape((qudit_dimension,) * num_qudits) # Number of extra elements needed in each dimension if represented using qubits. - padding_amount = _nearest_power_of_two_ceiling( - qudit_dimension) - qudit_dimension + padding_amount = _nearest_power_of_two_ceiling(qudit_dimension) - qudit_dimension # Expand the number of elements in each dimension by the padding_amount. Fill # the new elements with the _pad_value. - padded_state_tensor = np.pad(state_tensor, - pad_width=(0, padding_amount), - constant_values=_pad_value) + padded_state_tensor = np.pad( + state_tensor, pad_width=(0, padding_amount), constant_values=_pad_value + ) # Return a flattened state vector view of the final tensor. return np.ravel(padded_state_tensor) @@ -102,10 +101,9 @@ def qubit_to_qudit_state( """ mbit_dimension = _nearest_power_of_two_ceiling(qudit_dimension) # Reshape the state vector to a `num_qudits` rank tensor. - state_tensor = qubit_state_vector.reshape((mbit_dimension, ) * num_qudits) + state_tensor = qubit_state_vector.reshape((mbit_dimension,) * num_qudits) # Shrink the number of elements in each dimension up to the qudit_dimension, ignoring the rest. - trimmed_state_tensor = state_tensor[(slice(qudit_dimension), ) * - num_qudits] + trimmed_state_tensor = state_tensor[(slice(qudit_dimension),) * num_qudits] # Return a flattened state vector view of the final tensor. return np.ravel(trimmed_state_tensor) @@ -141,8 +139,7 @@ def qudit_to_qubit_unitary( A numpy array representing the input unitary using m-qubits-per-qudit. Expected shape: `((2 ^ m) ^ num_qudits, (2 ^ m) ^ num_qudits)`. """ - dim_qubit_space = _nearest_power_of_two_ceiling( - qudit_dimension)**num_qudits + dim_qubit_space = _nearest_power_of_two_ceiling(qudit_dimension) ** num_qudits if memoize: # Perform the transform of the below array from qubit to qudit space so that the indices @@ -161,14 +158,14 @@ def qudit_to_qubit_unitary( iter_range = range(qudit_dimension**num_qudits) for i, j in itertools.product(iter_range, iter_range): # Use the index map to populate the appropriate element in the qubit representation. - result[d_to_b_index_map[i]][ - d_to_b_index_map[j]] = qudit_unitary[i][j] + result[d_to_b_index_map[i]][d_to_b_index_map[j]] = qudit_unitary[i][j] return result # Treat the unitary as a num_qudits^2 system's state vector and represent it using qubits (pad # with 0s). - padded_unitary = qudit_to_qubit_state(qudit_dimension, num_qudits * 2, - np.ravel(qudit_unitary)) + padded_unitary = qudit_to_qubit_state( + qudit_dimension, num_qudits * 2, np.ravel(qudit_unitary) + ) # A qubit-based state vector with the extra padding bits having 1s and rest having 0s. This # vector marks only the bits that are padded. pad_qubits_vector = qudit_to_qubit_state( @@ -180,8 +177,9 @@ def qudit_to_qubit_unitary( # Reshape the padded unitary to the final shape and add a diagonal matrix corresponding to the # pad_qubits_vector. This addition ensures that the invalid states with the "padding" bits map # to identity, preserving unitarity. - return padded_unitary.reshape(dim_qubit_space, - dim_qubit_space) + np.diag(pad_qubits_vector) + return padded_unitary.reshape(dim_qubit_space, dim_qubit_space) + np.diag( + pad_qubits_vector + ) def qubit_to_qudit_unitary( @@ -212,11 +210,12 @@ def qubit_to_qudit_unitary( # Treat unitary as a `num_qudits*2` qudit system state vector. effective_num_qudits = num_qudits * 2 # Reshape the state vector to a `num_qudits*2` rank tensor. - unitary_tensor = qubit_unitary.reshape( - (mbit_dimension, ) * effective_num_qudits) + unitary_tensor = qubit_unitary.reshape((mbit_dimension,) * effective_num_qudits) # Shrink the number of elements in each dimension up to the qudit_dimension, ignoring the rest. - trimmed_unitary_tensor = unitary_tensor[(slice(qudit_dimension), ) * - effective_num_qudits] + trimmed_unitary_tensor = unitary_tensor[ + (slice(qudit_dimension),) * effective_num_qudits + ] # Return a flat unitary view of the final tensor. - return trimmed_unitary_tensor.reshape(qudit_dimension**num_qudits, - qudit_dimension**num_qudits) + return trimmed_unitary_tensor.reshape( + qudit_dimension**num_qudits, qudit_dimension**num_qudits + ) From dcf6c4dd50fbf27c1a3b5ff7c916dfc512980e27 Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 14:52:05 -0800 Subject: [PATCH 05/10] TicTacToe, more tests and reformatting --- unitary/alpha/quantum_world.py | 165 ++++++++++-------- unitary/alpha/quantum_world_test.py | 42 ++++- unitary/alpha/qudit_state_transform.py | 4 +- .../examples/tictactoe/tic_tac_split_test.py | 24 ++- unitary/examples/tictactoe/tic_tac_toe.py | 62 ++++--- .../examples/tictactoe/tic_tac_toe_test.py | 31 ++-- 6 files changed, 201 insertions(+), 127 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index a4cbb5e4..23d8e468 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -18,7 +18,7 @@ from unitary.alpha.quantum_object import QuantumObject from unitary.alpha.sparse_vector_simulator import PostSelectOperation, SparseSimulator -from unitary.alpha.qudit_state_transform import qudit_to_qubit_unitary, _num_bits +from unitary.alpha.qudit_state_transform import qudit_to_qubit_unitary, num_bits class QuantumWorld: @@ -83,8 +83,8 @@ def add_object(self, obj: QuantumObject): self.compiled_qubits[obj.qubit] = [obj.qubit] else: self.compiled_qubits[obj.qubit] = [] - for qubit_num in range(_num_bits(qudit_dim)): - new_obj = self.add_ancilla(obj.qubit.name) + for qubit_num in range(num_bits(qudit_dim)): + new_obj = self._add_ancilla(obj.qubit.name) self.compiled_qubits[obj.qubit].append(new_obj.qubit) obj.initial_effect() @@ -92,6 +92,13 @@ def add_object(self, obj: QuantumObject): def objects(self) -> List[QuantumObject]: return list(self.object_name_dict.values()) + @property + def public_objects(self) -> List[QuantumObject]: + return [ + obj for obj in self.object_name_dict.values() + if obj.name not in self.ancilla_names + ] + def get_object_by_name(self, name: str) -> Optional[QuantumObject]: """Returns the object with the given name. @@ -119,6 +126,7 @@ def combine_with(self, other_world: "QuantumWorld"): "Cannot combine sparse simulator world with non-sparse") self.object_name_dict.update(other_world.object_name_dict) self.ancilla_names.update(other_world.ancilla_names) + self.compiled_qubits.update(other_world.compiled_qubits) self.post_selection.update(other_world.post_selection) self.circuit = self.circuit.zip(other_world.circuit) # Clear effect history, since undoing would undo the combined worlds @@ -126,6 +134,28 @@ def combine_with(self, other_world: "QuantumWorld"): # Clear the other world so that objects cannot be used from that world. other_world.clear() + def _add_ancilla(self, + namespace: str, + value: Union[enum.Enum, int] = 0) -> QuantumObject: + """Adds an ancilla qudit object with a unique name. + + Args: + namespace: Custom string to be added in the name + value: The value for the ancilla qudit + + Returns: + The added ancilla object. + """ + count = 0 + ancilla_name = f"ancilla_{namespace}_{count}" + while ancilla_name in self.object_name_dict: + count += 1 + ancilla_name = f"ancilla_{namespace}_{count}" + new_obj = QuantumObject(ancilla_name, value) + self.add_object(new_obj) + self.ancilla_names.add(ancilla_name) + return new_obj + def _append_op(self, op: cirq.Operation): """Add the operation in a way designed to speed execution. @@ -140,35 +170,46 @@ def _append_op(self, op: cirq.Operation): strategy = cirq.InsertStrategy.NEW if self.compile_to_qubits: - qid_shape = cirq.qid_shape(op) - qudit_dim = max(qid_shape) - if qudit_dim > 2: - num_qudits = len(qid_shape) - new_unitary = qudit_to_qubit_unitary( - qudit_dimension=qudit_dim, - num_qudits=num_qudits, - qudit_unitary=cirq.unitary(op)) - num_qubits_per_qudit = _num_bits(qudit_dim) - num_qubits = num_qubits_per_qudit * num_qudits - all_qubits = [] - for qudit in op.qubits: - qubits = self.compiled_qubits[qudit] - if num_qubits_per_qudit > len(qubits): - # Original padding was not enough. This means that one - # of the qubits in the Operation has a higher dimension - # than this qubit. - for qubit_num in range(len(qubits), - num_qubits_per_qudit): - new_obj = self.add_ancilla(qudit.name) - qubits.append(new_obj.qubit) - all_qubits.extend(self.compiled_qubits[qudit]) - - op = cirq.MatrixGate(matrix=new_unitary, - qid_shape=(2, ) * - num_qubits).on(*all_qubits) + op = self._compile_op(op) self.circuit.append(op, strategy=strategy) + def _compile_op(self, op: cirq.Operation) -> cirq.Operation: + qid_shape = cirq.qid_shape(op) + qudit_dim = max(qid_shape) + if qudit_dim <= 2: + return op + num_qudits = len(qid_shape) + num_qubits_per_qudit = num_bits(qudit_dim) + num_qubits = num_qubits_per_qudit * num_qudits + all_qubits = [] + for qudit in op.qubits: + num_compiled_qubits = len(self.compiled_qubits[qudit]) + if num_qubits_per_qudit > num_compiled_qubits: + # Original padding was not enough. This means that one + # of the qubits in the Operation has a higher dimension + # than this qubit. + for qubit_num in range(num_compiled_qubits, + num_qubits_per_qudit): + new_obj = self._add_ancilla(qudit.name) + self.compiled_qubits[qudit].append(new_obj.qubit) + compiled_op_qubits = self.compiled_qubits[qudit] + elif num_qubits_per_qudit < num_compiled_qubits: + # This op needs less padding than what this qubit has. + # Use the first N qubits based on the size required. + compiled_op_qubits = self.compiled_qubits[ + qudit][:num_qubits_per_qudit] + else: + compiled_op_qubits = self.compiled_qubits[qudit] + + all_qubits.extend(compiled_op_qubits) + + new_unitary = qudit_to_qubit_unitary(qudit_dimension=qudit_dim, + num_qudits=num_qudits, + qudit_unitary=cirq.unitary(op)) + return cirq.MatrixGate(matrix=new_unitary, + qid_shape=(2, ) * num_qubits).on(*all_qubits) + def add_effect(self, op_list: List[cirq.Operation]): """Adds an operation to the current circuit.""" self.effect_history.append( @@ -203,35 +244,22 @@ def _suggest_num_reps(self, sample_size: int) -> int: sample_size = 100 return sample_size - def _interpret_result(self, result: Union[int, Iterable[int]]): + def _interpret_result(self, result: Union[int, Iterable[int]]) -> int: + """Canonicalize an entry from the measurement results array to int.""" if self.compile_to_qubits: + # For a compiled qudit, the results will be a bit array + # representing an integer outcome. return cirq.big_endian_bits_to_int(result) if isinstance(result, Iterable): + # If it is a single-element iterable, return the first element. + if len(result) != 1: + raise ValueError( + f"Cannot interpret a multivalued iterable {result} as a " + "single result for a non-compiled world." + ) return result[0] return result - def add_ancilla(self, - namespace: str, - value: Union[enum.Enum, int] = 0) -> QuantumObject: - """Adds an ancilla qudit object with a unique name. - - Args: - namespace: Custom string to be added in the name - value: The value for the ancilla qudit - - Returns: - The added ancilla object. - """ - count = 0 - ancilla_name = f"ancilla_{namespace}_{count}" - while ancilla_name in self.object_name_dict: - count += 1 - ancilla_name = f"ancilla_{namespace}_{count}" - new_obj = QuantumObject(ancilla_name, value) - self.add_object(new_obj) - self.ancilla_names.add(ancilla_name) - return new_obj - def force_measurement(self, obj: QuantumObject, result: Union[enum.Enum, int]) -> str: """Measures a QuantumObject with a defined outcome. @@ -241,20 +269,22 @@ def force_measurement(self, obj: QuantumObject, result: Union[enum.Enum, to be a particular result. A new qubit set to the initial state of the result. """ - new_obj = self.add_ancilla(namespace=obj.name, value=result) - if self.compile_to_qubits and self.compiled_qubits: + new_obj = self._add_ancilla(namespace=obj.name, value=result) + # Swap the input and ancilla qubits using a remapping dict. + qubit_remapping_dict = { + obj.qubit: new_obj.qubit, + new_obj.qubit: obj.qubit + } + if self.compile_to_qubits: + # Swap the compiled qubits. obj_qubits = self.compiled_qubits.get(obj.qubit, [obj.qubit]) new_obj_qubits = self.compiled_qubits.get(new_obj.qubit, [new_obj.qubit]) - qubit_remapping_dict = dict({ + qubit_remapping_dict.update({ *zip(obj_qubits, new_obj_qubits), *zip(new_obj_qubits, obj_qubits) }) - else: - qubit_remapping_dict = { - obj.qubit: new_obj.qubit, - new_obj.qubit: obj.qubit - } + self.circuit = self.circuit.transform_qubits( lambda q: qubit_remapping_dict.get(q, q)) post_selection = result.value if isinstance(result, @@ -292,10 +322,7 @@ def peek( measure_circuit = self.circuit.copy() if objects is None: - objects = [ - obj for obj in self.objects - if obj.name not in self.ancilla_names - ] + objects = self.public_objects measure_set = set(objects + list(self.post_selection.keys())) measure_circuit.append([ cirq.measure(self.compiled_qubits.get(p.qubit, p.qubit), @@ -343,10 +370,7 @@ def pop( self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection))) if objects is None: - objects = [ - obj for obj in self.objects - if obj.name not in self.ancilla_names - ] + objects = self.public_objects results = self.peek(objects, convert_to_enum=convert_to_enum) for idx, result in enumerate(results[0]): self.force_measurement(objects[idx], result) @@ -367,10 +391,7 @@ def get_histogram(self, counts for each state of the given object. """ if not objects: - objects = [ - obj for obj in self.objects - if obj.name not in self.ancilla_names - ] + objects = self.public_objects peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index a8e2f28e..9c16841b 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -13,8 +13,11 @@ # limitations under the License. # import enum + import pytest +import numpy as np + import cirq import unitary.alpha as alpha @@ -102,9 +105,44 @@ def test_two_qutrits(compile_to_qubits): [StopLight.YELLOW, StopLight.GREEN], [StopLight.YELLOW, StopLight.GREEN], ] - if not board.compile_to_qubits: + if board.compile_to_qubits: + g0 = cirq.NamedQubit("ancilla_green_0") + g1 = cirq.NamedQubit("ancilla_green_1") + y0 = cirq.NamedQubit("ancilla_yellow_0") + y1 = cirq.NamedQubit("ancilla_yellow_1") + # Flip the 0 and 2 states from identity for Green. + g_x02 = cirq.MatrixGate( + np.array([[0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]], + dtype=float)).on(g0, g1) + # Flip the 0 and 1 states from identity for Yellow. + y_x01 = cirq.MatrixGate( + np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], + dtype=float)).on(y0, y1) + circuit = cirq.Circuit(g_x02, y_x01) + expected = str(circuit) + else: expected = "green (d=3): ────X(0_2)───\n\nyellow (d=3): ───X(0_1)───" - assert str(board.circuit) == expected + assert str(board.circuit) == expected + + +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_qubit_and_qutrit(compile_to_qubits): + light = alpha.QuantumObject("yellow", Light.GREEN) + light2 = alpha.QuantumObject("green", StopLight.GREEN) + board = alpha.QuantumWorld([light, light2], + compile_to_qubits=compile_to_qubits) + assert board.peek(convert_to_enum=False) == [[1, 2]] + assert board.peek() == [[Light.GREEN, StopLight.GREEN]] + assert board.peek([light], count=2) == [[Light.GREEN], [Light.GREEN]] + assert board.peek([light2], count=2) == [[StopLight.GREEN], + [StopLight.GREEN]] + assert board.peek(count=3) == [ + [Light.GREEN, StopLight.GREEN], + [Light.GREEN, StopLight.GREEN], + [Light.GREEN, StopLight.GREEN], + ] + # Only the qutrit gets compiled to ancillas. + assert len(board.ancilla_names) == (2 if compile_to_qubits else 0) @pytest.mark.parametrize("compile_to_qubits", [False, True]) diff --git a/unitary/alpha/qudit_state_transform.py b/unitary/alpha/qudit_state_transform.py index 4754452c..2fe553c1 100644 --- a/unitary/alpha/qudit_state_transform.py +++ b/unitary/alpha/qudit_state_transform.py @@ -17,10 +17,10 @@ import numpy as np -def _num_bits(num: int) -> int: +def num_bits(num: int) -> int: """Returns the minimum number of bits needed to represent the input.""" result = 1 - while 2**result < num: + while num > 2**result: result += 1 return result diff --git a/unitary/examples/tictactoe/tic_tac_split_test.py b/unitary/examples/tictactoe/tic_tac_split_test.py index 6f536168..775818a9 100644 --- a/unitary/examples/tictactoe/tic_tac_split_test.py +++ b/unitary/examples/tictactoe/tic_tac_split_test.py @@ -82,6 +82,7 @@ def test_diagram(): ) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize( "mark, ruleset", [ @@ -91,10 +92,14 @@ def test_diagram(): (tictactoe.TicTacSquare.O, tictactoe.TicTacRules.QUANTUM_V3), ], ) -def test_tic_tac_split(mark: tictactoe.TicTacSquare, ruleset: tictactoe.TicTacRules): +def test_tic_tac_split( + mark: tictactoe.TicTacSquare, + ruleset: tictactoe.TicTacRules, + compile_to_qubits: bool, +): a = alpha.QuantumObject("a", tictactoe.TicTacSquare.EMPTY) b = alpha.QuantumObject("b", tictactoe.TicTacSquare.EMPTY) - board = alpha.QuantumWorld([a, b]) + board = alpha.QuantumWorld([a, b], compile_to_qubits=compile_to_qubits) tictactoe.TicTacSplit(mark, ruleset)(a, b) results = board.peek(count=1000) on_a = [mark, tictactoe.TicTacSquare.EMPTY] @@ -104,11 +109,12 @@ def test_tic_tac_split(mark: tictactoe.TicTacSquare, ruleset: tictactoe.TicTacRu assert all(r == on_a or r == on_b for r in results) -def test_tic_tac_split_entangled_v2(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_tic_tac_split_entangled_v2(compile_to_qubits): a = alpha.QuantumObject("a", tictactoe.TicTacSquare.EMPTY) b = alpha.QuantumObject("b", tictactoe.TicTacSquare.EMPTY) c = alpha.QuantumObject("c", tictactoe.TicTacSquare.EMPTY) - board = alpha.QuantumWorld([a, b, c]) + board = alpha.QuantumWorld([a, b, c], compile_to_qubits=compile_to_qubits) ruleset = tictactoe.TicTacRules.QUANTUM_V2 tictactoe.TicTacSplit(tictactoe.TicTacSquare.X, ruleset)(a, b) tictactoe.TicTacSplit(tictactoe.TicTacSquare.O, ruleset)(b, c) @@ -122,11 +128,12 @@ def test_tic_tac_split_entangled_v2(): assert all(r == on_ab or r == on_ac or r == on_b for r in results) -def test_tic_tac_split_entangled_v3(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_tic_tac_split_entangled_v3(compile_to_qubits): a = alpha.QuantumObject("a", tictactoe.TicTacSquare.EMPTY) b = alpha.QuantumObject("b", tictactoe.TicTacSquare.EMPTY) c = alpha.QuantumObject("c", tictactoe.TicTacSquare.EMPTY) - board = alpha.QuantumWorld([a, b, c]) + board = alpha.QuantumWorld([a, b, c], compile_to_qubits=compile_to_qubits) ruleset = tictactoe.TicTacRules.QUANTUM_V3 tictactoe.TicTacSplit(tictactoe.TicTacSquare.X, ruleset)(a, b) tictactoe.TicTacSplit(tictactoe.TicTacSquare.O, ruleset)(b, c) @@ -148,11 +155,12 @@ def test_tic_tac_split_entangled_v3(): ) -def test_tic_tac_split_entangled_v3_empty(): +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_tic_tac_split_entangled_v3_empty(compile_to_qubits): a = alpha.QuantumObject("a", tictactoe.TicTacSquare.EMPTY) b = alpha.QuantumObject("b", tictactoe.TicTacSquare.EMPTY) c = alpha.QuantumObject("c", tictactoe.TicTacSquare.EMPTY) - board = alpha.QuantumWorld([a, b, c]) + board = alpha.QuantumWorld([a, b, c], compile_to_qubits=compile_to_qubits) ruleset = tictactoe.TicTacRules.QUANTUM_V3 tictactoe.TicTacSplit(tictactoe.TicTacSquare.X, ruleset)(a, b) tictactoe.TicTacSplit(tictactoe.TicTacSquare.O, ruleset)(c, b) diff --git a/unitary/examples/tictactoe/tic_tac_toe.py b/unitary/examples/tictactoe/tic_tac_toe.py index e14cab37..a47d5f2c 100644 --- a/unitary/examples/tictactoe/tic_tac_toe.py +++ b/unitary/examples/tictactoe/tic_tac_toe.py @@ -19,11 +19,7 @@ from unitary.examples.tictactoe.tic_tac_split import TicTacSplit _SQUARE_NAMES = "abcdefghi" -_MARK_SYMBOLS = { - TicTacSquare.EMPTY: ".", - TicTacSquare.X: "X", - TicTacSquare.O: "O" -} +_MARK_SYMBOLS = {TicTacSquare.EMPTY: ".", TicTacSquare.X: "X", TicTacSquare.O: "O"} # Possible ways to get three in a row (rows, columns, and diagonal) indices _POSSIBLE_WINS = [ @@ -38,8 +34,7 @@ ] -def _histogram( - results: List[List[TicTacSquare]]) -> List[Dict[TicTacSquare, int]]: +def _histogram(results: List[List[TicTacSquare]]) -> List[Dict[TicTacSquare, int]]: """Turns a list of whole board measurements into a histogram. Returns: @@ -48,11 +43,7 @@ def _histogram( """ hist = [] for idx in range(9): - hist.append({ - TicTacSquare.EMPTY: 0, - TicTacSquare.X: 0, - TicTacSquare.O: 0 - }) + hist.append({TicTacSquare.EMPTY: 0, TicTacSquare.X: 0, TicTacSquare.O: 0}) for r in results: for idx in range(9): hist[idx][r[idx]] += 1 @@ -108,9 +99,10 @@ class TicTacToe: human-friendly text format or by using `sample()` to get one or more samples (measurements) of the board. """ - def __init__(self, - rules: TicTacRules = TicTacRules.QUANTUM_V3, - run_on_hardware: bool = False): + + def __init__( + self, rules: TicTacRules = TicTacRules.QUANTUM_V3, run_on_hardware: bool = False + ): self.clear(run_on_hardware) self.rules = rules @@ -125,8 +117,9 @@ def clear(self, run_on_hardware: bool = False) -> None: for name in _SQUARE_NAMES: self.empty_squares.add(name) self.squares[name] = QuantumObject(name, TicTacSquare.EMPTY) - self.board = QuantumWorld(list(self.squares.values()), - compile_to_qubits=run_on_hardware) + self.board = QuantumWorld( + list(self.squares.values()), compile_to_qubits=run_on_hardware + ) def result(self) -> TicTacResult: """Returns the result of the TicTacToe game. @@ -163,13 +156,13 @@ def move(self, move: str, mark: TicTacSquare) -> TicTacResult: if len(move) > 2 or len(move) == 0: raise ValueError(f"Your move ({move}) must be one or two letters.") if not all(m in _SQUARE_NAMES for m in move): - raise ValueError( - f"Your move ({move}) can only have these: {_SQUARE_NAMES}") + raise ValueError(f"Your move ({move}) can only have these: {_SQUARE_NAMES}") if len(move) == 1: # Check if the square is empty if move not in self.empty_squares: raise ValueError( - f"You cannot put your token on non-empty square {move}") + f"You cannot put your token on non-empty square {move}" + ) # Flip the square to the correct value (mark.value) QuditFlip(3, 0, mark.value)(self.squares[move]) # This square is now no longer empty @@ -178,26 +171,33 @@ def move(self, move: str, mark: TicTacSquare) -> TicTacResult: # Check if rules allow quantum moves if self.rules == TicTacRules.CLASSICAL: raise ValueError( - f"Quantum moves are not allowed in a classical TicTacToe") + f"Quantum moves are not allowed in a classical TicTacToe" + ) # Check if either square is non-empty. Splitting on top of # non-empty squares is only allowed at full quantumness - if ((move[0] not in self.empty_squares) or - (move[1] not in self.empty_squares)) and ( - self.rules == TicTacRules.QUANTUM_V1): + if ( + (move[0] not in self.empty_squares) + or (move[1] not in self.empty_squares) + ) and (self.rules == TicTacRules.QUANTUM_V1): raise ValueError( f"This ruleset ({0}) does not allow splits on \ - top of non-empty squares".format(self.rules)) + top of non-empty squares".format( + self.rules + ) + ) # TicTacSplit first flips the first square before performing a split # If either of the two involved squares is empty, we want to do the # split on that square. if move[1] in self.empty_squares: - TicTacSplit(mark, self.rules)(self.squares[move[1]], - self.squares[move[0]]) + TicTacSplit(mark, self.rules)( + self.squares[move[1]], self.squares[move[0]] + ) else: - TicTacSplit(mark, self.rules)(self.squares[move[0]], - self.squares[move[1]]) + TicTacSplit(mark, self.rules)( + self.squares[move[0]], self.squares[move[1]] + ) # The involved squares are now no longer empty self.empty_squares.discard(move[0]) @@ -222,9 +222,7 @@ def sample(self, count: int = 1) -> List[str]: would return '.X.XO..OO'. """ - return [ - _result_to_str(result) for result in self.board.peek(count=count) - ] + return [_result_to_str(result) for result in self.board.peek(count=count)] def print(self) -> str: """Returns the TicTacToe board in ASCII form.""" diff --git a/unitary/examples/tictactoe/tic_tac_toe_test.py b/unitary/examples/tictactoe/tic_tac_toe_test.py index bb911c33..153f4704 100644 --- a/unitary/examples/tictactoe/tic_tac_toe_test.py +++ b/unitary/examples/tictactoe/tic_tac_toe_test.py @@ -53,8 +53,9 @@ def test_eval_board(result, expected): assert tictactoe.tic_tac_toe.eval_board(result) == expected -def test_measure(): - board = tictactoe.TicTacToe() +@pytest.mark.parametrize("run_on_hardware", [False, True]) +def test_measure(run_on_hardware): + board = tictactoe.TicTacToe(run_on_hardware=run_on_hardware) board.move("ab", tictactoe.TicTacSquare.X) board.move("cd", tictactoe.TicTacSquare.O) board.move("ef", tictactoe.TicTacSquare.X) @@ -95,8 +96,9 @@ def test_measure(): assert all(result == expected for result in results) -def test_sample(): - board = tictactoe.TicTacToe() +@pytest.mark.parametrize("run_on_hardware", [False, True]) +def test_sample(run_on_hardware): + board = tictactoe.TicTacToe(run_on_hardware=run_on_hardware) board.move("a", tictactoe.TicTacSquare.X) board.move("e", tictactoe.TicTacSquare.O) results = board.sample(count=100) @@ -109,8 +111,9 @@ def test_sample(): assert all(result == "X...O..OX" for result in results) -def test_split(): - board = tictactoe.TicTacToe() +@pytest.mark.parametrize("run_on_hardware", [False, True]) +def test_split(run_on_hardware): + board = tictactoe.TicTacToe(run_on_hardware=run_on_hardware) board.move("ab", tictactoe.TicTacSquare.X) results = board.sample(count=1000) assert len(results) == 1000 @@ -121,21 +124,27 @@ def test_split(): assert all(result == in_a or result == in_b for result in results) -def test_rulesets(): +@pytest.mark.parametrize("run_on_hardware", [False, True]) +def test_rulesets(run_on_hardware): # Try to make a quantum move with classical rules - board = tictactoe.TicTacToe(tictactoe.TicTacRules.CLASSICAL) + board = tictactoe.TicTacToe( + tictactoe.TicTacRules.CLASSICAL, run_on_hardware=run_on_hardware + ) with pytest.raises(ValueError): board.move("ab", tictactoe.TicTacSquare.X) # Try to make a split move on non-empty squares at minimal quantum - board = tictactoe.TicTacToe(tictactoe.TicTacRules.QUANTUM_V1) + board = tictactoe.TicTacToe( + tictactoe.TicTacRules.QUANTUM_V1, run_on_hardware=run_on_hardware + ) board.move("a", tictactoe.TicTacSquare.X) with pytest.raises(ValueError): board.move("ab", tictactoe.TicTacSquare.O) -def test_print(): - board = tictactoe.TicTacToe() +@pytest.mark.parametrize("run_on_hardware", [False, True]) +def test_print(run_on_hardware): + board = tictactoe.TicTacToe(run_on_hardware=run_on_hardware) board.move("a", tictactoe.TicTacSquare.X) board.move("e", tictactoe.TicTacSquare.O) output = board.print() From b4f9f7463d1624fbcaca963ae40167b3babed864 Mon Sep 17 00:00:00 2001 From: Smit Date: Mon, 26 Dec 2022 22:48:59 -0800 Subject: [PATCH 06/10] Refactor --- unitary/alpha/quantum_world.py | 60 +++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 23d8e468..fd499cf0 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -94,6 +94,7 @@ def objects(self) -> List[QuantumObject]: @property def public_objects(self) -> List[QuantumObject]: + """All non-ancilla objects in the world.""" return [ obj for obj in self.object_name_dict.values() if obj.name not in self.ancilla_names @@ -174,9 +175,41 @@ def _append_op(self, op: cirq.Operation): self.circuit.append(op, strategy=strategy) + def _get_num_compiled_qubits(self, qudit: cirq.Qid, + num_expected_qubits: int) -> List[cirq.Qid]: + """Gets the exact number of compiled qubits for the qudit. + + May create extra ancilla qubits, if needed. Currently, none of the + non-trivial cases are exercised because our actual qubit compilation + does not support mixing of different qudit dimensions. + """ + num_compiled_qubits = len(self.compiled_qubits[qudit]) + if num_expected_qubits > num_compiled_qubits: + # Original padding was not enough. This means that one + # of the qubits in the Operation has a higher dimension + # than this qubit. + for qubit_num in range(num_compiled_qubits, num_expected_qubits): + new_obj = self._add_ancilla(qudit.name) + self.compiled_qubits[qudit].append(new_obj.qubit) + return self.compiled_qubits[qudit] + if num_expected_qubits < num_compiled_qubits: + # This op needs less padding than what this qubit has. + # Use the first N qubits based on the size required. + return self.compiled_qubits[qudit][:num_expected_qubits] + else: + return self.compiled_qubits[qudit] + def _compile_op(self, op: cirq.Operation) -> cirq.Operation: + """Compiles the operation down to qubits, if needed.""" qid_shape = cirq.qid_shape(op) - qudit_dim = max(qid_shape) + distinct_dims = set(qid_shape) + if len(distinct_dims) > 1: + # TODO: Add support for arbitrary Qid shapes to + # `qudit_state_transform`. + raise ValueError( + f"Found operation shape {qid_shape}. Compiling operations with" + " a mix of different dimensioned qudits is not supported yet.") + qudit_dim = distinct_dims.pop() if qudit_dim <= 2: return op num_qudits = len(qid_shape) @@ -184,26 +217,10 @@ def _compile_op(self, op: cirq.Operation) -> cirq.Operation: num_qubits = num_qubits_per_qudit * num_qudits all_qubits = [] for qudit in op.qubits: - num_compiled_qubits = len(self.compiled_qubits[qudit]) - if num_qubits_per_qudit > num_compiled_qubits: - # Original padding was not enough. This means that one - # of the qubits in the Operation has a higher dimension - # than this qubit. - for qubit_num in range(num_compiled_qubits, - num_qubits_per_qudit): - new_obj = self._add_ancilla(qudit.name) - self.compiled_qubits[qudit].append(new_obj.qubit) - compiled_op_qubits = self.compiled_qubits[qudit] - elif num_qubits_per_qudit < num_compiled_qubits: - # This op needs less padding than what this qubit has. - # Use the first N qubits based on the size required. - compiled_op_qubits = self.compiled_qubits[ - qudit][:num_qubits_per_qudit] - else: - compiled_op_qubits = self.compiled_qubits[qudit] - - all_qubits.extend(compiled_op_qubits) + all_qubits.extend( + self._get_num_compiled_qubits(qudit, num_qubits_per_qudit)) + # Compile the input unitary to a target qubit-based unitary. new_unitary = qudit_to_qubit_unitary(qudit_dimension=qudit_dim, num_qudits=num_qudits, qudit_unitary=cirq.unitary(op)) @@ -255,8 +272,7 @@ def _interpret_result(self, result: Union[int, Iterable[int]]) -> int: if len(result) != 1: raise ValueError( f"Cannot interpret a multivalued iterable {result} as a " - "single result for a non-compiled world." - ) + "single result for a non-compiled world.") return result[0] return result From 3252562904f51898c1a7788b18d28b9fa1e8f1ca Mon Sep 17 00:00:00 2001 From: Smit Date: Sun, 15 Jan 2023 17:30:24 -0800 Subject: [PATCH 07/10] Address comments, add more tests --- unitary/alpha/quantum_effect.py | 2 +- unitary/alpha/quantum_world.py | 64 +++---- unitary/alpha/quantum_world_test.py | 163 ++++++++++++++++++ unitary/alpha/sparse_vector_simulator_test.py | 9 + unitary/examples/tictactoe/tic_tac_toe.py | 3 + 5 files changed, 201 insertions(+), 40 deletions(-) diff --git a/unitary/alpha/quantum_effect.py b/unitary/alpha/quantum_effect.py index d08a0735..d3fe058d 100644 --- a/unitary/alpha/quantum_effect.py +++ b/unitary/alpha/quantum_effect.py @@ -49,7 +49,7 @@ def _verify_objects(self, *objects): q.num_states != required_dimension ): raise ValueError( - f"Cannot apply effect to qids of dimension {required_dimension}." + f"Cannot apply effect to qids of dimension {q.num_states}." ) if q.world is None: raise ValueError( diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index fd499cf0..d95049e2 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -36,6 +36,11 @@ class QuantumWorld: This object should be initialized with a sampler that determines how to evaluate the quantum game state. If not specified, this defaults to the built-in cirq Simulator. + + Setting the `compile_to_qubits` option results in an internal state + representation of ancilla qubits for every qudit in the world. That + also results in the effects being applied to the corresponding qubits + instead of the original qudits. """ def __init__(self, objects: Optional[List[QuantumObject]] = None, @@ -175,57 +180,31 @@ def _append_op(self, op: cirq.Operation): self.circuit.append(op, strategy=strategy) - def _get_num_compiled_qubits(self, qudit: cirq.Qid, - num_expected_qubits: int) -> List[cirq.Qid]: - """Gets the exact number of compiled qubits for the qudit. - - May create extra ancilla qubits, if needed. Currently, none of the - non-trivial cases are exercised because our actual qubit compilation - does not support mixing of different qudit dimensions. - """ - num_compiled_qubits = len(self.compiled_qubits[qudit]) - if num_expected_qubits > num_compiled_qubits: - # Original padding was not enough. This means that one - # of the qubits in the Operation has a higher dimension - # than this qubit. - for qubit_num in range(num_compiled_qubits, num_expected_qubits): - new_obj = self._add_ancilla(qudit.name) - self.compiled_qubits[qudit].append(new_obj.qubit) - return self.compiled_qubits[qudit] - if num_expected_qubits < num_compiled_qubits: - # This op needs less padding than what this qubit has. - # Use the first N qubits based on the size required. - return self.compiled_qubits[qudit][:num_expected_qubits] - else: - return self.compiled_qubits[qudit] - def _compile_op(self, op: cirq.Operation) -> cirq.Operation: """Compiles the operation down to qubits, if needed.""" qid_shape = cirq.qid_shape(op) - distinct_dims = set(qid_shape) - if len(distinct_dims) > 1: + if len(set(qid_shape)) > 1: # TODO: Add support for arbitrary Qid shapes to # `qudit_state_transform`. raise ValueError( f"Found operation shape {qid_shape}. Compiling operations with" " a mix of different dimensioned qudits is not supported yet.") - qudit_dim = distinct_dims.pop() - if qudit_dim <= 2: + qudit_dim = qid_shape[0] + if qudit_dim == 2: return op num_qudits = len(qid_shape) - num_qubits_per_qudit = num_bits(qudit_dim) - num_qubits = num_qubits_per_qudit * num_qudits - all_qubits = [] + compiled_qubits = [] for qudit in op.qubits: - all_qubits.extend( - self._get_num_compiled_qubits(qudit, num_qubits_per_qudit)) + compiled_qubits.extend(self.compiled_qubits[qudit]) # Compile the input unitary to a target qubit-based unitary. - new_unitary = qudit_to_qubit_unitary(qudit_dimension=qudit_dim, - num_qudits=num_qudits, - qudit_unitary=cirq.unitary(op)) - return cirq.MatrixGate(matrix=new_unitary, - qid_shape=(2, ) * num_qubits).on(*all_qubits) + compiled_unitary = qudit_to_qubit_unitary( + qudit_dimension=qudit_dim, + num_qudits=num_qudits, + qudit_unitary=cirq.unitary(op)) + return cirq.MatrixGate(matrix=compiled_unitary, + qid_shape=(2, ) * + len(compiled_qubits)).on(*compiled_qubits) def add_effect(self, op_list: List[cirq.Operation]): """Adds an operation to the current circuit.""" @@ -262,7 +241,14 @@ def _suggest_num_reps(self, sample_size: int) -> int: return sample_size def _interpret_result(self, result: Union[int, Iterable[int]]) -> int: - """Canonicalize an entry from the measurement results array to int.""" + """Canonicalize an entry from the measurement results array to int. + + When `compile_to_qubit` is set, the results are expected to be a + sequence of bits that are the binary representation of the measurement + of the original key. Returns the `int` represented by the bits. + + If the input is a single-element Iterable, returns the first element. + """ if self.compile_to_qubits: # For a compiled qudit, the results will be a bit array # representing an integer outcome. diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index 9c16841b..40cec8f6 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -21,6 +21,7 @@ import cirq import unitary.alpha as alpha +import unitary.alpha.qudit_gates as qudit_gates class Light(enum.Enum): @@ -123,6 +124,7 @@ def test_two_qutrits(compile_to_qubits): else: expected = "green (d=3): ────X(0_2)───\n\nyellow (d=3): ───X(0_1)───" assert str(board.circuit) == expected + assert board.pop() == [StopLight.YELLOW, StopLight.GREEN] @pytest.mark.parametrize("compile_to_qubits", [False, True]) @@ -143,6 +145,7 @@ def test_qubit_and_qutrit(compile_to_qubits): ] # Only the qutrit gets compiled to ancillas. assert len(board.ancilla_names) == (2 if compile_to_qubits else 0) + assert board.pop() == [Light.GREEN, StopLight.GREEN] @pytest.mark.parametrize("compile_to_qubits", [False, True]) @@ -166,6 +169,48 @@ def test_pop(simulator, compile_to_qubits): assert all(result[1] != popped for result in results) +# TODO: Consider moving to qudit_effects.py if this can be broadly useful. +class QuditSwapEffect(alpha.QuantumEffect): + def __init__(self, dimension): + self.dimension = dimension + + def effect(self, object1, object2): + yield qudit_gates.QuditSwapPowGate(self.dimension)(object1.qubit, + object2.qubit) + + +# TODO: Consider moving to qudit_effects.py if this can be broadly useful. +class QuditSplitEffect(alpha.QuantumEffect): + def __init__(self, dimension): + self.dimension = dimension + + def effect(self, object1, object2, object3): + yield qudit_gates.QuditSwapPowGate(self.dimension, 0.5)(object1.qubit, + object2.qubit) + yield qudit_gates.QuditSwapPowGate(self.dimension, 1)(object1.qubit, + object3.qubit) + + +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_pop_qudit(compile_to_qubits): + light = alpha.QuantumObject("l1", StopLight.GREEN) + light2 = alpha.QuantumObject("l2", StopLight.RED) + light3 = alpha.QuantumObject("l3", StopLight.RED) + board = alpha.QuantumWorld([light, light2, light3], + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + QuditSplitEffect(3)(light, light2, light3) + results = board.peek([light2, light3], count=200) + assert all(result[0] != result[1] for result in results) + assert not all(result[0] == 0 for result in results) + assert not all(result[0] == 1 for result in results) + popped = board.pop([light2])[0] + results = board.peek([light2, light3], count=200) + assert len(results) == 200 + assert all(result[0] == popped for result in results) + assert all(result[1] != popped for result in results) + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) def test_pop_and_reuse(simulator, compile_to_qubits): @@ -181,6 +226,20 @@ def test_pop_and_reuse(simulator, compile_to_qubits): assert popped == Light.RED +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_pop_and_reuse_qudit(compile_to_qubits): + """Tests reusing a popped qudit.""" + light = alpha.QuantumObject("l1", StopLight.GREEN) + board = alpha.QuantumWorld([light], + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + popped = board.pop([light])[0] + assert popped == StopLight.GREEN + alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(light) + popped = board.pop([light])[0] + assert popped == StopLight.RED + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) def test_undo(simulator, compile_to_qubits): @@ -196,6 +255,20 @@ def test_undo(simulator, compile_to_qubits): assert all(result[0] == Light.GREEN for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_undo_qudit(compile_to_qubits): + light = alpha.QuantumObject("l1", StopLight.GREEN) + board = alpha.QuantumWorld([light], + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(light) + results = board.peek([light], count=200) + assert all(result[0] == StopLight.RED for result in results) + board.undo_last_effect() + results = board.peek([light], count=200) + assert all(result[0] == StopLight.GREEN for result in results) + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) def test_undo_no_effects(compile_to_qubits): board = alpha.QuantumWorld([], compile_to_qubits=compile_to_qubits) @@ -236,6 +309,38 @@ def test_undo_post_select(simulator, compile_to_qubits): assert not all(result[0] == Light.GREEN for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_undo_post_select_qudits(compile_to_qubits): + light = alpha.QuantumObject("l1", StopLight.GREEN) + light2 = alpha.QuantumObject("l2", StopLight.RED) + light3 = alpha.QuantumObject("l3", StopLight.RED) + board = alpha.QuantumWorld([light, light2, light3], + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + QuditSplitEffect(3)(light, light2, light3) + + # After split, should be fifty-fifty + results = board.peek([light2, light3], count=200) + assert all(result[0] != result[1] for result in results) + assert not all(result[0] == StopLight.RED for result in results) + assert not all(result[0] == StopLight.GREEN for result in results) + + # After pop, should be consistently one value + popped = board.pop([light2])[0] + results = board.peek([light2, light3], count=200) + assert len(results) == 200 + assert all(result[0] == popped for result in results) + assert all(result[1] != popped for result in results) + + # After undo, should be fifty-fifty + board.undo_last_effect() + results = board.peek([light2, light3], count=200) + assert len(results) == 200 + assert all(result[0] != result[1] for result in results) + assert not all(result[0] == StopLight.RED for result in results) + assert not all(result[0] == StopLight.GREEN for result in results) + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) def test_pop_not_enough_reps(simulator, compile_to_qubits): @@ -270,6 +375,38 @@ def test_pop_not_enough_reps(simulator, compile_to_qubits): for result in results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_pop_not_enough_reps_qudits(compile_to_qubits): + """Tests forcing a measurement of a rare outcome, + so that peek needs to be called recursively to get more + occurances. + """ + lights = [ + alpha.QuantumObject("l" + str(i), StopLight.RED) for i in range(9) + ] + board = alpha.QuantumWorld(lights, + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(lights[0]) + QuditSplitEffect(3)(lights[0], lights[1], lights[2]) + QuditSplitEffect(3)(lights[2], lights[3], lights[4]) + QuditSplitEffect(3)(lights[4], lights[5], lights[6]) + QuditSplitEffect(3)(lights[6], lights[7], lights[8]) + + results = board.peek([lights[8]], count=2000) + assert any(result[0] == StopLight.GREEN for result in results) + assert not all(result[0] == StopLight.GREEN for result in results) + board.force_measurement(lights[8], StopLight.GREEN) + + results = board.peek([lights[8]], count=200) + assert len(results) == 200 + assert all(result[0] == StopLight.GREEN for result in results) + results = board.peek(count=200) + assert all(len(result) == 9 for result in results) + assert all(result == [StopLight.RED] * 8 + [StopLight.GREEN] + for result in results) + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) @pytest.mark.parametrize("simulator", [cirq.Simulator, alpha.SparseSimulator]) def test_pop_qubits_twice(simulator, compile_to_qubits): @@ -295,6 +432,32 @@ def test_pop_qubits_twice(simulator, compile_to_qubits): assert all(peek_result == result2 for peek_result in peek_results) +@pytest.mark.parametrize("compile_to_qubits", [False, True]) +def test_pop_qudits_twice(compile_to_qubits): + """Tests popping qudits twice, so that 2 ancillas are created + for each qudit.""" + lights = [ + alpha.QuantumObject("l" + str(i), StopLight.RED) for i in range(3) + ] + board = alpha.QuantumWorld(lights, + sampler=cirq.Simulator(), + compile_to_qubits=compile_to_qubits) + alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(lights[0]) + QuditSplitEffect(3)(lights[0], lights[1], lights[2]) + result = board.pop() + green_on_1 = [StopLight.RED, StopLight.GREEN, StopLight.RED] + green_on_2 = [StopLight.RED, StopLight.RED, StopLight.GREEN] + assert (result == green_on_1) or (result == green_on_2) + QuditSwapEffect(3)(lights[1], lights[2]) + result2 = board.pop() + peek_results = board.peek(count=200) + if result == green_on_1: + assert result2 == green_on_2 + else: + assert result2 == green_on_1 + assert all(peek_result == result2 for peek_result in peek_results) + + @pytest.mark.parametrize("compile_to_qubits", [False, True]) def test_combine_overlapping_worlds(compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) diff --git a/unitary/alpha/sparse_vector_simulator_test.py b/unitary/alpha/sparse_vector_simulator_test.py index 3ab607cf..fa7c397b 100644 --- a/unitary/alpha/sparse_vector_simulator_test.py +++ b/unitary/alpha/sparse_vector_simulator_test.py @@ -2,6 +2,7 @@ import cirq from cirq.testing import random_circuit +from unitary.alpha import qudit_gates from unitary.alpha.sparse_vector_simulator import ( SparseSimulator, @@ -41,6 +42,14 @@ def test_simulation_fidelity(): assert abs(test_ones_fraction - validation_ones_fraction) < 0.1 +def test_simulation_fidelity_qudits_fails(): + """Check that SparseSimulator does not support Qudit operations yet.""" + qudit = cirq.NamedQid("a", 3) + circuit = cirq.Circuit(qudit_gates.QuditXGate(3).on(qudit), cirq.measure(qudit)) + with pytest.raises(ValueError, match="size 2 is different from 3"): + _ = SparseSimulator().run(circuit).measurements + + def test_post_selection(): """Test a circuit with PostSelectOperation.""" sim = SparseSimulator() diff --git a/unitary/examples/tictactoe/tic_tac_toe.py b/unitary/examples/tictactoe/tic_tac_toe.py index a47d5f2c..770413a0 100644 --- a/unitary/examples/tictactoe/tic_tac_toe.py +++ b/unitary/examples/tictactoe/tic_tac_toe.py @@ -98,6 +98,9 @@ class TicTacToe: Results can be viewed by using `print()` to see the board in a human-friendly text format or by using `sample()` to get one or more samples (measurements) of the board. + + Set `run_on_hardware` to compile the board to qubits for running on + actual hardware. """ def __init__( From 03ee4fab094712488965798acb11f45b6771f5dc Mon Sep 17 00:00:00 2001 From: Smit Date: Sun, 15 Jan 2023 19:13:05 -0800 Subject: [PATCH 08/10] Support qudit operations in SparseSimulator via compilation --- unitary/alpha/quantum_world.py | 15 ++- unitary/alpha/quantum_world_test.py | 121 ++++++++++++++---- unitary/alpha/sparse_vector_simulator_test.py | 5 +- 3 files changed, 112 insertions(+), 29 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index d95049e2..76cb04bc 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -180,11 +180,12 @@ def _append_op(self, op: cirq.Operation): self.circuit.append(op, strategy=strategy) - def _compile_op(self, op: cirq.Operation) -> cirq.Operation: + def _compile_op(self, + op: cirq.Operation) -> Union[cirq.Operation, cirq.OP_TREE]: """Compiles the operation down to qubits, if needed.""" qid_shape = cirq.qid_shape(op) if len(set(qid_shape)) > 1: - # TODO: Add support for arbitrary Qid shapes to + # TODO(#77): Add support for arbitrary Qid shapes to # `qudit_state_transform`. raise ValueError( f"Found operation shape {qid_shape}. Compiling operations with" @@ -197,6 +198,16 @@ def _compile_op(self, op: cirq.Operation) -> cirq.Operation: for qudit in op.qubits: compiled_qubits.extend(self.compiled_qubits[qudit]) + if isinstance(op, PostSelectOperation): + # Spread the post-selected value across the compiled qubits using the + # big endian convention. + value_bits = cirq.big_endian_int_to_bits( + op.value, bit_count=len(compiled_qubits)) + return [ + PostSelectOperation(qubit, value) + for qubit, value in zip(compiled_qubits, value_bits) + ] + # Compile the input unitary to a target qubit-based unitary. compiled_unitary = qudit_to_qubit_unitary( qudit_dimension=qudit_dim, diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index 40cec8f6..b93b51e6 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -127,11 +127,19 @@ def test_two_qutrits(compile_to_qubits): assert board.pop() == [StopLight.YELLOW, StopLight.GREEN] -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_qubit_and_qutrit(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_qubit_and_qutrit(simulator, compile_to_qubits): light = alpha.QuantumObject("yellow", Light.GREEN) light2 = alpha.QuantumObject("green", StopLight.GREEN) board = alpha.QuantumWorld([light, light2], + sampler=simulator(), compile_to_qubits=compile_to_qubits) assert board.peek(convert_to_enum=False) == [[1, 2]] assert board.peek() == [[Light.GREEN, StopLight.GREEN]] @@ -191,13 +199,20 @@ def effect(self, object1, object2, object3): object3.qubit) -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_pop_qudit(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_pop_qudit(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", StopLight.GREEN) light2 = alpha.QuantumObject("l2", StopLight.RED) light3 = alpha.QuantumObject("l3", StopLight.RED) board = alpha.QuantumWorld([light, light2, light3], - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) QuditSplitEffect(3)(light, light2, light3) results = board.peek([light2, light3], count=200) @@ -226,12 +241,19 @@ def test_pop_and_reuse(simulator, compile_to_qubits): assert popped == Light.RED -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_pop_and_reuse_qudit(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_pop_and_reuse_qudit(simulator, compile_to_qubits): """Tests reusing a popped qudit.""" light = alpha.QuantumObject("l1", StopLight.GREEN) board = alpha.QuantumWorld([light], - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) popped = board.pop([light])[0] assert popped == StopLight.GREEN @@ -255,11 +277,18 @@ def test_undo(simulator, compile_to_qubits): assert all(result[0] == Light.GREEN for result in results) -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_undo_qudit(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_undo_qudit(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", StopLight.GREEN) board = alpha.QuantumWorld([light], - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(light) results = board.peek([light], count=200) @@ -309,13 +338,20 @@ def test_undo_post_select(simulator, compile_to_qubits): assert not all(result[0] == Light.GREEN for result in results) -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_undo_post_select_qudits(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_undo_post_select_qudits(simulator, compile_to_qubits): light = alpha.QuantumObject("l1", StopLight.GREEN) light2 = alpha.QuantumObject("l2", StopLight.RED) light3 = alpha.QuantumObject("l3", StopLight.RED) board = alpha.QuantumWorld([light, light2, light3], - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) QuditSplitEffect(3)(light, light2, light3) @@ -375,8 +411,15 @@ def test_pop_not_enough_reps(simulator, compile_to_qubits): for result in results) -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_pop_not_enough_reps_qudits(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_pop_not_enough_reps_qudits(simulator, compile_to_qubits): """Tests forcing a measurement of a rare outcome, so that peek needs to be called recursively to get more occurances. @@ -385,7 +428,7 @@ def test_pop_not_enough_reps_qudits(compile_to_qubits): alpha.QuantumObject("l" + str(i), StopLight.RED) for i in range(9) ] board = alpha.QuantumWorld(lights, - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(lights[0]) QuditSplitEffect(3)(lights[0], lights[1], lights[2]) @@ -432,15 +475,22 @@ def test_pop_qubits_twice(simulator, compile_to_qubits): assert all(peek_result == result2 for peek_result in peek_results) -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_pop_qudits_twice(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_pop_qudits_twice(simulator, compile_to_qubits): """Tests popping qudits twice, so that 2 ancillas are created for each qudit.""" lights = [ alpha.QuantumObject("l" + str(i), StopLight.RED) for i in range(3) ] board = alpha.QuantumWorld(lights, - sampler=cirq.Simulator(), + sampler=simulator(), compile_to_qubits=compile_to_qubits) alpha.QuditFlip(3, StopLight.RED.value, StopLight.GREEN.value)(lights[0]) QuditSplitEffect(3)(lights[0], lights[1], lights[2]) @@ -545,11 +595,20 @@ def test_get_histogram_and_get_probabilities_one_binary_qobject( assert 0.1 <= bin_probs[0] <= 1.0 -@pytest.mark.parametrize("compile_to_qubits", [False, True]) +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) def test_get_histogram_and_get_probabilities_one_trinary_qobject( - compile_to_qubits): + simulator, compile_to_qubits): l1 = alpha.QuantumObject("l1", StopLight.YELLOW) - world = alpha.QuantumWorld([l1], compile_to_qubits=compile_to_qubits) + world = alpha.QuantumWorld([l1], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) histogram = world.get_histogram() assert histogram == [{0: 0, 1: 100, 2: 0}] probs = world.get_probabilities() @@ -558,11 +617,21 @@ def test_get_histogram_and_get_probabilities_one_trinary_qobject( assert bin_probs == [1.0] -@pytest.mark.parametrize("compile_to_qubits", [False, True]) -def test_get_histogram_and_get_probabilities_two_qobjects(compile_to_qubits): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ]) +def test_get_histogram_and_get_probabilities_two_qobjects( + simulator, compile_to_qubits): l1 = alpha.QuantumObject("l1", Light.GREEN) l2 = alpha.QuantumObject("l2", StopLight.YELLOW) - world = alpha.QuantumWorld([l1, l2], compile_to_qubits=compile_to_qubits) + world = alpha.QuantumWorld([l1, l2], + sampler=simulator(), + compile_to_qubits=compile_to_qubits) histogram = world.get_histogram() assert histogram == [{0: 0, 1: 100}, {0: 0, 1: 100, 2: 0}] probs = world.get_probabilities() diff --git a/unitary/alpha/sparse_vector_simulator_test.py b/unitary/alpha/sparse_vector_simulator_test.py index fa7c397b..40e88729 100644 --- a/unitary/alpha/sparse_vector_simulator_test.py +++ b/unitary/alpha/sparse_vector_simulator_test.py @@ -43,7 +43,10 @@ def test_simulation_fidelity(): def test_simulation_fidelity_qudits_fails(): - """Check that SparseSimulator does not support Qudit operations yet.""" + """Check that SparseSimulator does not support Qudit operations yet. + + TODO(#78): Fix this. + """ qudit = cirq.NamedQid("a", 3) circuit = cirq.Circuit(qudit_gates.QuditXGate(3).on(qudit), cirq.measure(qudit)) with pytest.raises(ValueError, match="size 2 is different from 3"): From 9d8815ca3ced4d9151edc2c6cff6e520d45e860f Mon Sep 17 00:00:00 2001 From: Smit Date: Wed, 1 Feb 2023 13:52:17 -0800 Subject: [PATCH 09/10] Add comment for compiled_qubits dict --- unitary/alpha/quantum_world.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 76cb04bc..d8b68b25 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -67,6 +67,8 @@ def clear(self): self.effect_history = [] self.object_name_dict: Dict[str, QuantumObject] = {} self.ancilla_names = set() + # When `compile_to_qubits` is True, this tracks the mapping of the + # original qudits to the compiled qubits. self.compiled_qubits: Dict[cirq.Qid, List[cirq.Qid]] = {} self.post_selection: Dict[QuantumObject, int] = {} From 005cc52f9cd6399ef6f8055ce1ca8ecd0e61ca16 Mon Sep 17 00:00:00 2001 From: Smit Date: Wed, 1 Feb 2023 14:03:09 -0800 Subject: [PATCH 10/10] Appease lint checker for Windows --- unitary/quantum_chess/ascii_board.py | 1 - unitary/quantum_chess/circuit_transformer.py | 1 - 2 files changed, 2 deletions(-) diff --git a/unitary/quantum_chess/ascii_board.py b/unitary/quantum_chess/ascii_board.py index a0ffd721..31219177 100644 --- a/unitary/quantum_chess/ascii_board.py +++ b/unitary/quantum_chess/ascii_board.py @@ -17,7 +17,6 @@ class AsciiBoard: """ def __init__(self, size: int = 8, reps: int = 1000, board=None): - self.size = size self.reps = reps self.board = board or qb.CirqBoard(0) diff --git a/unitary/quantum_chess/circuit_transformer.py b/unitary/quantum_chess/circuit_transformer.py index ec3c85ed..300708ce 100644 --- a/unitary/quantum_chess/circuit_transformer.py +++ b/unitary/quantum_chess/circuit_transformer.py @@ -402,7 +402,6 @@ def __call__( def transform_controlled_iswap(op: cirq.Operation, index: int) -> cirq.OP_TREE: - if len(op.qubits) <= 3 and isinstance(op, cirq.ControlledOperation): if not all(v == 1 for values in op.control_values for v in values): return op