diff --git a/unitary/alpha/quantum_effect.py b/unitary/alpha/quantum_effect.py index f60623ba..3259080c 100644 --- a/unitary/alpha/quantum_effect.py +++ b/unitary/alpha/quantum_effect.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from typing import Iterator, Optional, Sequence, Union +from typing import Iterator, Optional, Sequence, Union, TYPE_CHECKING import abc import enum import cirq +if TYPE_CHECKING: + from unitary.alpha.quantum_object import QuantumObject + def _to_int(value: Union[enum.Enum, int]) -> int: return value.value if isinstance(value, enum.Enum) else value @@ -25,7 +28,7 @@ def _to_int(value: Union[enum.Enum, int]) -> int: class QuantumEffect(abc.ABC): @abc.abstractmethod - def effect(self, *objects) -> Iterator[cirq.Operation]: + def effect(self, *objects: "QuantumObject") -> Iterator[cirq.Operation]: """Apply the Quantum Effect to the QuantumObjects.""" def num_dimension(self) -> Optional[int]: @@ -39,7 +42,7 @@ def num_objects(self) -> Optional[int]: """ return None - def _verify_objects(self, *objects): + def _verify_objects(self, *objects: "QuantumObject"): if self.num_objects() is not None and len(objects) != self.num_objects(): raise ValueError(f"Cannot apply effect to {len(objects)} qubits.") @@ -56,7 +59,7 @@ def _verify_objects(self, *objects): "Object must be added to a QuantumWorld to apply effects." ) - def __call__(self, *objects): + def __call__(self, *objects: "QuantumObject"): """Apply the Quantum Effect to the objects.""" self._verify_objects(*objects) world = objects[0].world @@ -86,15 +89,15 @@ class QuantumIf: must equal the number of control qubits. """ - def effect(self, *objects) -> Iterator[cirq.Operation]: + def effect(self, *objects: "QuantumObject") -> Iterator[cirq.Operation]: return iter(()) - def __call__(self, *objects): + def __call__(self, *objects: "QuantumObject"): return QuantumThen(*objects) class QuantumThen(QuantumEffect): - def __init__(self, *objects): + def __init__(self, *objects: "QuantumObject"): self.control_objects = list(objects) self.condition = [1] * len(self.control_objects) self.then_effect = None @@ -126,7 +129,7 @@ def apply(self, effect: "QuantumEffect"): self.then_effect = effect return self - def effect(self, *objects): + def effect(self, *objects: "QuantumObject"): """A Quantum if/then produces a controlled operation.""" # For anti-controls, add an X before the controlled operation for idx, cond in enumerate(self.condition): diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index fad477e5..924da2ce 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -21,7 +21,15 @@ MoveType, MoveVariant, ) -from unitary.examples.quantum_chinese_chess.move import Jump +from unitary.examples.quantum_chinese_chess.move import ( + Jump, + SplitJump, + MergeJump, + Slide, + SplitSlide, + MergeSlide, + CannonFire, +) import readline # List of accepable commands. @@ -390,6 +398,7 @@ def apply_move(self, str_to_parse: str) -> None: print(move_type, " ", move_variant) # Apply the move accoding to its type. + # TODO(): using match...case... when python 3.11 satisfies the dependency. if move_type == MoveType.CLASSICAL: if source_0.type_ == Type.KING: # Update the locations of KING. @@ -402,7 +411,18 @@ def apply_move(self, str_to_parse: str) -> None: Jump(move_variant)(source_0, target_0) elif move_type == MoveType.JUMP: Jump(move_variant)(source_0, target_0) - # TODO(): apply other move types. + elif move_type == MoveType.SLIDE: + Slide(quantum_pieces_0, move_variant)(source_0, target_0) + elif move_type == MoveType.SPLIT_JUMP: + SplitJump()(source_0, target_0, target_1) + elif move_type == MoveType.SPLIT_SLIDE: + SplitSlide(quantum_pieces_0, quantum_pieces_1)(source_0, target_0, target_1) + elif move_type == MoveType.MERGE_JUMP: + MergeJump()(source_0, source_1, target_0) + elif move_type == MoveType.MERGE_SLIDE: + MergeSlide(quantum_pieces_0, quantum_pieces_1)(source_0, source_1, target_0) + elif move_type == MoveType.CANNON_FIRE: + CannonFire(classical_pieces_0, quantum_pieces_0)(source_0, target_0) def next_move(self) -> Tuple[bool, str]: """Check if the player wants to exit or needs help message. Otherwise parse and apply the move. diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 9a6a8398..f1fddcfa 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -187,6 +187,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # Pass the classical properties of the source piece to the target pieces. target_0.reset(source_0) target_1.reset(source_0) + # Note that we should not reset source_0 (to be empty) since there are cases where + # the source is not empty after the move. We'll later use board.update_board_by_sampling() + # to determine if the source piece needs to be reset to be empty. return iter(()) @@ -217,3 +220,417 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # Pass the classical properties of the source pieces to the target piece. target_0.reset(source_0) return iter(()) + + +class Slide(QuantumEffect): + """Slide from source_0 to target_0, with quantum_path_pieces_0 being the quantum pieces + along the path. The accepted move_variant includes + - CAPTURE + - EXCLUDED + - BASIC + + Types of pieces that could make Slide moves are: CANNON, ROOK, HORSE, and ELEPHANT. + """ + + def __init__( + self, + quantum_path_pieces_0: List[str], + move_variant: MoveVariant, + ): + self.quantum_path_pieces_0 = quantum_path_pieces_0 + self.move_variant = move_variant + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 2 + + def effect(self, *objects) -> Iterator[cirq.Operation]: + source_0, target_0 = objects + world = source_0.world + quantum_path_pieces_0 = [world[path] for path in self.quantum_path_pieces_0] + if self.move_variant == MoveVariant.EXCLUDED: + target_is_occupied = world.pop([target_0])[0].value + # For excluded slide, we need to measure the target piece and only make the slide when it's not there. + if target_is_occupied: + print("Slide move not applied: target turns out to be occupied.") + # Set the target to be a classical piece. + target_0.is_entangled = False + return iter(()) + # If the target is measured to be empty, then we reset its classical properties to be empty. + target_0.reset() + elif self.move_variant == MoveVariant.CAPTURE: + could_capture = False + if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: + # We consider the special case when the source is classical and there is + # only one quantum piece in the path, to save an ancilla. + if not world.pop(quantum_path_pieces_0)[0].value: + # If the only quantum path piece turns out to be empty, we reset it to be + # classically EMPTY and will do the capture later. + quantum_path_pieces_0[0].reset() + could_capture = True + else: + # For the case where either the source piece is entangled or there are more than + # one quantum path piece, we create and measure a capture ancilla to determine if + # the slide could be made. + source_0.is_entangled = True + capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + control_qubits = [source_0] + quantum_path_pieces_0 + # We could do the slide only if source is there and all quantum path pieces + # are empty. + conditions = [1] + [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*control_qubits).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) + # We measure the ancilla to dertermine whether the slide could be made. + could_capture = world.pop([capture_ancilla])[0] + if not could_capture: + print( + "Slide move not applied: either the source turns out be empty, or the path turns out to be blocked." + ) + return iter(()) + # Apply the capture. + # Force measure the source to be there. + world.force_measurement(source_0, 1) + source_0.is_entangled = False + # Target qubit is unhooked, i.e. it's replaced with a new ancilla with value = 0. + world.unhook(target_0) + # Now that it's qubit is reset, we also reset its classical properties. + target_0.reset() + alpha.PhasedMove()(source_0, target_0) + # Move the classical properties of the source piece to the target piece. + target_0.reset(source_0) + source_0.reset() + # Force measure the whole path to be empty. + for path_piece in quantum_path_pieces_0: + world.force_measurement(path_piece, 0) + path_piece.reset() + return iter(()) + # For BASIC or EXCLUDED cases. + # Note that we DON'T require that the source piece is there. + source_0.is_entangled = True + conditions = [0] * len(quantum_path_pieces_0) + # We will apply the slide only if all quantum path pieces are empty. + alpha.quantum_if(*quantum_path_pieces_0).equals(*conditions).apply( + alpha.PhasedMove() + )(source_0, target_0) + # Copy the classical properties of the source piece to the target piece. + target_0.reset(source_0) + # Note that we should not reset source_0 (to be empty) since there are cases where + # the source is not moved. We'll later use board.update_board_by_sampling() to determine + # if the source piece needs to be reset to be empty. + return iter(()) + + +class SplitSlide(QuantumEffect): + """SplitSlide from source_0 to target_0 and target_1, with quantum_path_pieces_0 being the + quantum path pieces from source_0 to target_0, and quantum_path_pieces_1 being the quantum + path pieces from source_0 to target_1. The only accepted (default) move_variant is + - BASIC + + Types of pieces that could make SplitSlide moves are: CANNON, ROOK, HORSE, and ELEPHANT. + """ + + def __init__( + self, + quantum_path_pieces_0: List[str], + quantum_path_pieces_1: List[str], + ): + self.quantum_path_pieces_0 = quantum_path_pieces_0 + self.quantum_path_pieces_1 = quantum_path_pieces_1 + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 3 + + def effect(self, *objects) -> Iterator[cirq.Operation]: + source_0, target_0, target_1 = objects + world = source_0.world + # In the cases where two paths overlap, we remove the other target from the path. + # TODO(): maybe we don't need this check since currently we only support move_variant + # = BASIC, which means two target pieces are classically empty. + quantum_path_pieces_0 = [ + world[path] for path in self.quantum_path_pieces_0 if path != target_1.name + ] + quantum_path_pieces_1 = [ + world[path] for path in self.quantum_path_pieces_1 if path != target_0.name + ] + source_0.is_entangled = True + if len(quantum_path_pieces_0) == 0 and len(self.quantum_path_pieces_1) == 0: + # If both paths are empty, do split jump instead. + # TODO(): maybe move the above checks (if any path piece is one of the target pieces) + # into classify_move(). This is currently a redundant check. + SplitJump()(source_0, target_0, target_1) + return iter(()) + # Add a new ancilla to represent whether path 0 is clear (value 1 means clear). + # TODO(): save ancillas for some specific scenarios. + path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + # We flip the ancilla only if all quantum path pieces in path 0 are empty. + conditions = [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*quantum_path_pieces_0).equals(*conditions).apply( + alpha.Flip() + )(path_0_clear_ancilla) + + # Add a new ancilla to represent whether path 1 is clear (value 1 means clear). + path_1_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_1.name}") + # We flip the ancilla only if all quantum path pieces in path 1 are empty. + conditions = [0] * len(quantum_path_pieces_1) + alpha.quantum_if(*quantum_path_pieces_1).equals(*conditions).apply( + alpha.Flip() + )(path_1_clear_ancilla) + + # We do the normal split if both paths are clear. + # TODO(): should we change quantum if to support apply multiple effects when one + # if condition is met? + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 1).apply( + alpha.PhasedMove(0.5) + )(source_0, target_0) + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 1).apply( + alpha.PhasedMove() + )(source_0, target_1) + + # Else if only path 0 is clear, we ISWAP source_0 and target_0. + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 0).apply( + alpha.PhasedMove() + )(source_0, target_0) + + # Else if only path 1 is clear, we ISWAP source_0 and target_1. + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(0, 1).apply( + alpha.PhasedMove() + )(source_0, target_1) + + # Note: we do not zero-out, i.e. reverse those ancillas. But we could do this + # if we want to reuse them. + + # Pass the classical properties of the source piece to the target pieces. + target_0.reset(source_0) + target_1.reset(source_0) + # Note that we should not reset source_0 (to be empty) here since either slide arm could have + # entangled piece in the path which results in a non-zero probability that the source is not + # moved. We'll later use board.update_board_by_sampling() to determine if the source piece + # needs to be reset to be empty. + return iter(()) + + +class MergeSlide(QuantumEffect): + """MergeSlide from source_0 and source_1 to target_0, with quantum_path_pieces_0 being the + quantum path pieces from source_0 to target_0, and quantum_path_pieces_1 being the quantum + path pieces from source_1 to target_0. The only accepted (default) move_variant is + - BASIC + + Types of pieces that could make MergeSlide moves are: CANNON, ROOK, HORSE, and ELEPHANT. + """ + + def __init__( + self, + quantum_path_pieces_0: List[str], + quantum_path_pieces_1: List[str], + ): + self.quantum_path_pieces_0 = quantum_path_pieces_0 + self.quantum_path_pieces_1 = quantum_path_pieces_1 + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 3 + + def effect(self, *objects) -> Iterator[cirq.Operation]: + source_0, source_1, target_0 = objects + world = source_0.world + quantum_path_pieces_0 = [ + world[path] for path in self.quantum_path_pieces_0 if path != source_1.name + ] + quantum_path_pieces_1 = [ + world[path] for path in self.quantum_path_pieces_1 if path != source_0.name + ] + target_0.is_entangled = True + if len(quantum_path_pieces_0) == 0 and len(self.quantum_path_pieces_1) == 0: + # If both paths are empty, do merge jump instead. + # TODO(): maybe move the above checks (if any path piece is one of the source pieces) + # into classify_move(). + MergeJump()(source_0, source_1, target_0) + return iter(()) + + # TODO(): save ancillas for some specific scenarios. + # Add a new ancilla to represent whether path 0 is clear (value 1 means clear). + path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + path_0_conditions = [0] * len(quantum_path_pieces_0) + # We flip the ancilla (to have value 1) only if all path pieces in path 0 are empty. + alpha.quantum_if(*quantum_path_pieces_0).equals(*path_0_conditions).apply( + alpha.Flip() + )(path_0_clear_ancilla) + + # Add a new ancilla to represent whether path 1 is clear (value 1 means clear). + path_1_clear_ancilla = world._add_ancilla(f"{source_1.name}{target_0.name}") + path_1_conditions = [0] * len(quantum_path_pieces_1) + # We flip the ancilla (to have value 1) only if all path pieces in path 1 are empty. + alpha.quantum_if(*quantum_path_pieces_1).equals(*path_1_conditions).apply( + alpha.Flip() + )(path_1_clear_ancilla) + + # We do the normal merge if both paths are clear. + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 1).apply( + alpha.PhasedMove(-1.0) + )(source_0, target_0) + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 1).apply( + alpha.PhasedMove(-0.5) + )(source_1, target_0) + + # Else if only path 0 is clear, we ISWAP source_0 and target_0. + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(1, 0).apply( + alpha.PhasedMove(-1.0) + )(source_0, target_0) + + # Else if only path 1 is clear, we ISWAP source_1 and target_0. + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(0, 1).apply( + alpha.PhasedMove(-1.0) + )(source_1, target_0) + + # TODO(): Do we need to zero-out, i.e. reverse those ancillas? + # Pass the classical properties of the source pieces to the target piece. + target_0.reset(source_0) + # Note that we should not reset source_0 or source_1 (to be empty) here since either slide arm could have + # entangled piece in the path which results in a non-zero probability that the source is not moved. We'll + # later use board.update_board_by_sampling() to determine if any source piece needs to be reset to be empty. + return iter(()) + + +class CannonFire(QuantumEffect): + """CannonFire from source_0 to target_0, with classical_path_pieces_0 being the classical path pieces + along the path, and quantum_path_pieces_0 being the quantum path pieces along the path. + The only accepted (default) move_variant is + - CAPTURE. + + The only type of piece that could make CannonFire move is CANNON. + """ + + def __init__( + self, + classical_path_pieces_0: List[str], + quantum_path_pieces_0: List[str], + ): + self.classical_path_pieces_0 = classical_path_pieces_0 + self.quantum_path_pieces_0 = quantum_path_pieces_0 + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 2 + + def effect(self, *objects) -> Iterator[cirq.Operation]: + source_0, target_0 = objects + world = source_0.world + quantum_path_pieces_0 = [world[path] for path in self.quantum_path_pieces_0] + # Source has to be there to fire. + if source_0.is_entangled and not world.pop([source_0])[0].value: + source_0.reset() + print("Cannonn fire not applied: source turns out to be empty.") + return iter(()) + source_0.is_entangled = False + # Target has to be there to fire. + if target_0.is_entangled and not world.pop([target_0])[0].value: + target_0.reset() + print("Cannonn fire not applied: target turns out to be empty.") + return iter(()) + target_0.is_entangled = False + if len(self.classical_path_pieces_0) == 1: + # In the case where there already is a classical cannon platform, the cannon could + # fire and capture only if quantum_path_pieces_0 are all empty. + could_capture = False + if len(quantum_path_pieces_0) == 0: + could_capture = True + elif len(quantum_path_pieces_0) == 1: + # Consider this special case to save an ancilla. + # When there is 1 classical path piece and 1 quantum path piece, The cannon + # could fire only if the quantum path piece is empty. + if not world.pop(quantum_path_pieces_0)[0].value: + # Reset it to be classically empty. + quantum_path_pieces_0[0].reset() + could_capture = True + else: + # We add a new ancilla to indicate whether the capture could happen (value 1 means it could). + capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + control_objects = [source_0] + quantum_path_pieces_0 + conditions = [1] + [0] * len(quantum_path_pieces_0) + # We flip the ancilla only if the source is there and all quantum path pieces are empty. + alpha.quantum_if(*control_objects).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) + # We measure this ancilla to determine if the cannon fire could be made. + could_capture = world.pop([capture_ancilla])[0] + if not could_capture: + print("Cannon fire not applied: the path turns out to be blocked.") + return iter(()) + # Apply the capture. + # Reset the target qubit. + world.unhook(target_0) + # Reset the classical properties of the target. + target_0.reset() + alpha.PhasedMove()(source_0, target_0) + # Move the classical properties of the source piece to the target piece. + target_0.reset(source_0) + source_0.reset() + # Force measure all quantum_path_pieces_0 to be empty. + for path_piece in quantum_path_pieces_0: + if path_piece.is_entangled: + # We check if the piece is entangled since in the case len(quantum_path_pieces_0) == 1 + # the force_measurement has already been made. + world.force_measurement(path_piece, 0) + path_piece.reset() + return iter(()) + else: + # In the case where there are no classical path piece but only quantum + # path piece(s), the cannon could fire and capture only if there is exactly + # one quantum path piece being occupied. + could_capture = False + # We loop over all quantum path pieces and check if it could be the only + # occupied piece. The fire could be made if it does, otherwise not. + # TODO(): think a more efficient way of implementing this case. + for index, expect_occupied_path_piece in enumerate(quantum_path_pieces_0): + # TODO(): consider specific cases to save the ancilla. + # Add a new ancilla to indicate whether the fire could be made (value = 1 means it could). + capture_ancilla = world._add_ancilla(expect_occupied_path_piece.name) + # All other path pieces are expected to be empty to make the fire happen. + expect_empty_pieces = [ + piece + for piece in quantum_path_pieces_0 + if piece.name != expect_occupied_path_piece.name + ] + control_qubits = [ + source_0, + expect_occupied_path_piece, + ] + expect_empty_pieces + conditions = [1, 1] + [0] * len(expect_empty_pieces) + # We flip the ancilla only if source is there, expect_occupied_path_piece is there, + # and all other path pieces are empty. + alpha.quantum_if(*control_qubits).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) + # We measure the ancilla to determine if the fire could be made. + could_capture = world.pop([capture_ancilla])[0] + if could_capture: + # Apply the capture. + world.unhook(target_0) + target_0.reset() + alpha.PhasedMove()(source_0, target_0) + # Move the classical properties of the source piece to the target piece. + target_0.reset(source_0) + source_0.reset() + # Force measure all expect_empty_pieces to be empty. + for empty_path_piece in expect_empty_pieces: + world.force_measurement(empty_path_piece, 0) + empty_path_piece.reset() + # Force measure the current expect_occupied_path_piece to be occupied. + world.force_measurement(expect_occupied_path_piece, 1) + expect_occupied_path_piece.is_entangled = False + return iter(()) + # Reaching the end of the for loop means the fire could not be made. + print( + "Cannon fire not applied: there turns out to be (!=1) occupied path pieces." + ) + return iter(()) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 48c74fd1..ec901de5 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -386,3 +386,616 @@ def test_merge_jump_imperfect_merge_scenario_3(): locations_to_bitboard(["b2", "c2"]): 1.0 / 4, }, ) + + +def test_slide_basic_classical_source(): + """Source in classical state.""" + board = set_board(["a1", "b1"]) + world = board.board + SplitJump()(world["b1"], world["b2"], world["b3"]) + + Slide(["b2"], MoveVariant.BASIC)(world["a1"], world["c1"]) + + assert_sample_distribution( + board, + { + locations_to_bitboard(["a1", "b2"]): 1.0 / 2, + locations_to_bitboard(["b3", "c1"]): 1.0 / 2, # success + }, + ) + + +def test_slide_basic_quantum_source(): + """Source in quantum state.""" + board = set_board(["a1", "b1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + + Slide(["b2"], MoveVariant.EXCLUDED)(world["a2"], world["c1"]) + + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2"]): 1.0 / 4, + locations_to_bitboard(["a3", "b2"]): 1.0 / 4, + locations_to_bitboard(["c1", "b3"]): 1.0 / 4, # success + locations_to_bitboard(["a3", "b3"]): 1.0 / 4, + }, + ) + + +def test_slide_basic_quantum_source_with_path_qubits(): + """Source in quantum state + multiple path qubits.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + Slide(["b2", "c2"], MoveVariant.EXCLUDED)(world["a2"], world["d1"]) + + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b2", "c3"]): 1.0 / 8, + locations_to_bitboard(["a2", "b3", "c2"]): 1.0 / 8, + locations_to_bitboard(["d1", "b3", "c3"]): 1.0 / 8, # success + locations_to_bitboard(["a3", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c3"]): 1.0 / 8, + }, + ) + + +def test_slide_excluded_classical_source(): + """Source in classical state.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + Slide(["b2"], MoveVariant.EXCLUDED)(world["a1"], world["c2"]) + + # We check the ancilla to learn if the slide was applied or not. + target_is_occupied = world.post_selection[world["ancilla_c2_0"]] + if target_is_occupied: + # a1 is not moved, while both b2 and b3 are possible. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a1", "b2", "c2"]): 1.0 / 2, + locations_to_bitboard(["a1", "b3", "c2"]): 1.0 / 2, + }, + ) + else: + # a1 could move to c2 if b2 is not there. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a1", "b2", "c3"]): 1.0 / 2, + locations_to_bitboard(["b3", "c2", "c3"]): 1.0 / 2, # success + }, + ) + + +def test_slide_excluded_classical_source(): + """Source in quantum state.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + Slide(["b2"], MoveVariant.EXCLUDED)(world["a2"], world["c2"]) + + # We check the ancilla to learn if the slide was applied or not. + target_is_occupied = world.post_selection[world["ancilla_c2_0"]] + if target_is_occupied: + # a2 is not moved, while both b2 and b3 are possible. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a3", "b2", "c2"]): 1.0 / 4, + locations_to_bitboard(["a3", "b3", "c2"]): 1.0 / 4, + locations_to_bitboard(["a2", "b2", "c2"]): 1.0 / 4, + locations_to_bitboard(["a2", "b3", "c2"]): 1.0 / 4, + }, + ) + else: + # a2 could move to c2 if b2 is not there. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c3"]): 1.0 / 4, + locations_to_bitboard(["a3", "b2", "c3"]): 1.0 / 4, + locations_to_bitboard(["b3", "c2", "c3"]): 1.0 / 4, # success + locations_to_bitboard(["a3", "b3", "c3"]): 1.0 / 4, + }, + ) + + +def test_slide_excluded_quantum_source_with_path_qubits(): + """Source in quantum state + multiple path qubits.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + Slide(["b2", "c2"], MoveVariant.EXCLUDED)(world["a2"], world["d2"]) + + # We check the ancilla to learn if the slide was applied or not. + target_is_occupied = world.post_selection[world["ancilla_d2_0"]] + if target_is_occupied: + # a2 is not moved, while all path qubits combinations are possible. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b3", "c3", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 8, + }, + ) + else: + # a2 could move to d2 if both b2 and c2 are not there. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 8, + locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["d2", "b3", "c3", "d3"]): 1.0 / 8, # success + locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 8, + }, + ) + + +def test_slide_capture_classical_source_one_path_qubit(): + """Source is in classical state + only one path qubit.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + Slide(["b2"], MoveVariant.CAPTURE)(world["a1"], world["c2"]) + + # We check the ancilla to learn if the slide was applied or not. + path_is_blocked = world.post_selection[world["ancilla_b2_0"]] + if path_is_blocked: + # a1 is not moved, while both c2 and c3 are possible. + assert_sample_distribution( + board, + { + locations_to_bitboard(["a1", "b2", "c2"]): 1.0 / 2, + locations_to_bitboard(["a1", "b2", "c3"]): 1.0 / 2, + }, + ) + else: + # a1 moves to c2. + assert_sample_distribution( + board, + { + locations_to_bitboard(["b3", "c2"]): 1.0 / 2, # slided and captured + locations_to_bitboard(["b3", "c2", "c3"]): 1.0 + / 2, # slided but not captured + }, + ) + + +def test_slide_capture_quantum_source_multiple_path_qubits(): + """Source in quantum state + multiple path qubits.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + Slide(["b2", "c2"], MoveVariant.CAPTURE)(world["a2"], world["d2"]) + + # We check the ancilla to learn if the jump was applied or not. + # Note: at first there is a ancilla named ancilla_a2d2_0 created. + # then another ancilla ancilla_ancilla_a2d2_0_0 is created during the + # force measurement of ancilla_a2d2_0. + captured = world.post_selection[world["ancilla_ancilla_a2d2_0_0"]] + if captured: + # a2 is moved to d2, and the path is clear. + assert_sample_distribution( + board, + { + locations_to_bitboard(["b3", "c3", "d2"]): 1.0 + / 2, # slided and captured + locations_to_bitboard(["b3", "c3", "d2", "d3"]): 1.0 + / 2, # slided but not captured + }, + ) + else: + # The slide is not made, either because source is not there, or the path is blocked. + assert_sample_distribution( + board, + { + # cases with blocked path + locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 14, + locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1.0 / 14, + locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 14, + locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1.0 / 14, + locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 14, + locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1.0 / 14, + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 14, + locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 14, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 14, + locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 14, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 14, + locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 14, + # cases where the source is not there + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 14, + locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 14, + }, + ) + + +def test_split_slide_classical_source_one_path_clear(): + """Source is in classical state + one path is clear.""" + board = set_board(["a1", "b1"]) + world = board.board + SplitJump()(world["b1"], world["b2"], world["b3"]) + + SplitSlide(["b2"], [])(world["a1"], world["c1"], world["c2"]) + + assert_sample_distribution( + board, + { + locations_to_bitboard(["b2", "c2"]): 1.0 / 2, + locations_to_bitboard(["b3", "c1"]): 1.0 / 4, + locations_to_bitboard(["b3", "c2"]): 1.0 / 4, + }, + ) + + +def test_split_slide_quantum_source_multiple_path_qubits(): + """Source in quantum state + multiple path qubits.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + SplitSlide(["b2", "c2"], ["d2"])(world["a2"], world["e1"], world["e2"]) + + assert_sample_distribution( + board, + { + # both paths blocked + locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 16, + # path 0 is clear + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 16, + locations_to_bitboard(["e1", "b3", "c3", "d2"]): 1.0 / 16, # slide to e1 + # path 1 is clear + locations_to_bitboard(["e2", "b2", "c2", "d3"]): 1.0 / 16, # slide to e2 + locations_to_bitboard(["e2", "b3", "c2", "d3"]): 1.0 / 16, # slide to e2 + locations_to_bitboard(["e2", "b2", "c3", "d3"]): 1.0 / 16, # slide to e2 + locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 16, + locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 16, + locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 16, + # both paths are clear + locations_to_bitboard(["e1", "b3", "c3", "d3"]): 1.0 / 32, # slide to e1 + locations_to_bitboard(["e2", "b3", "c3", "d3"]): 1.0 / 32, # slide to e2 + locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 16, + }, + ) + + +def test_split_slide_quantum_source_overlapped_paths(): + """Source in quantum state + overlapped paths.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + SplitSlide(["b2", "c2"], ["b2"])(world["a2"], world["d1"], world["e1"]) + + assert_sample_distribution( + board, + { + # both paths blocked + locations_to_bitboard(["a2", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b2", "c3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3"]): 1.0 / 8, + # path 1 is clear + locations_to_bitboard(["e1", "b3", "c2"]): 1.0 / 8, # slide to e1 + locations_to_bitboard(["a3", "b3", "c2"]): 1.0 / 8, + # both paths are clear + locations_to_bitboard(["e1", "b3", "c3"]): 1.0 / 16, # slide to e1 + locations_to_bitboard(["d1", "b3", "c3"]): 1.0 / 16, # slide to d1 + locations_to_bitboard(["a3", "b3", "c3"]): 1.0 / 8, + }, + ) + + +def test_merge_slide_one_path_clear(): + """One path is clear.""" + board = set_board(["a1", "b1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + + MergeSlide(["b2"], [])(world["a2"], world["a3"], world["c1"]) + + assert_sample_distribution( + board, + { + locations_to_bitboard(["b2", "c1"]): 1.0 / 4, + locations_to_bitboard(["b2", "a2"]): 1.0 / 4, + locations_to_bitboard(["b3", "c1"]): 1.0 / 2, + }, + ) + + +def test_merge_slide_multiple_path_qubits(): + """Multiple path qubits.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + MergeSlide(["b2", "c2"], ["d2"])(world["a2"], world["a3"], world["e1"]) + + assert_sample_distribution( + board, + { + # both paths blocked + locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 16, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 16, + # path 0 is clear + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 16, + locations_to_bitboard(["e1", "b3", "c3", "d2"]): 1.0 / 16, # success + # path 1 is clear + locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1.0 / 16, + locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1.0 / 16, + locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1.0 / 16, + locations_to_bitboard(["e1", "b2", "c2", "d3"]): 1.0 / 16, # success + locations_to_bitboard(["e1", "b3", "c2", "d3"]): 1.0 / 16, # success + locations_to_bitboard(["e1", "b2", "c3", "d3"]): 1.0 / 16, # success + # both paths are clear + locations_to_bitboard(["e1", "b3", "c3", "d3"]): 1.0 / 8, # success + }, + ) + + +def test_merge_slide_overlapped_paths(): + """Overlapped paths.""" + board = set_board(["a1", "b1", "c1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + + MergeSlide(["b2", "c2"], ["b2"])(world["a2"], world["a3"], world["d1"]) + + assert_sample_distribution( + board, + { + # both paths blocked + locations_to_bitboard(["a2", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c2"]): 1.0 / 8, + locations_to_bitboard(["a2", "b2", "c3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3"]): 1.0 / 8, + # path 1 is clear + locations_to_bitboard(["d1", "b3", "c2"]): 1.0 / 8, # success + locations_to_bitboard(["a2", "b3", "c2"]): 1.0 / 8, + # both paths are clear + locations_to_bitboard(["d1", "b3", "c3"]): 1.0 / 4, # success + }, + ) + + +def test_cannon_fire_classical_source_target(): + """There are one classical piece and one quantum piece in path + both source and target are classical.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["c1"], world["c2"], world["c3"]) + + CannonFire(["b1"], ["c2"])(world["a1"], world["d1"]) + + # We check the ancilla to learn if the fire was applied or not. + path_is_blocked = world.post_selection[world["ancilla_c2_0"]] + + if not path_is_blocked: + assert_samples_in(board, {locations_to_bitboard(["b1", "c3", "d1"]): 1.0}) + else: + assert_samples_in(board, {locations_to_bitboard(["a1", "b1", "c2", "d1"]): 1.0}) + + +def test_cannon_fire_quantum_source_target(): + # There are one classical piece and one quantum piece in path + both source and target are quantum. + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + CannonFire(["b1"], ["c2"])(world["a2"], world["d2"]) + + # We check the ancilla to learn if the fire was applied or not. + source_is_occupied = world.post_selection[world["ancilla_a2_0"]] + if not source_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a3", "b1", "c2", "d2"]): 1.0 / 4, + locations_to_bitboard(["a3", "b1", "c2", "d3"]): 1.0 / 4, + locations_to_bitboard(["a3", "b1", "c3", "d2"]): 1.0 / 4, + locations_to_bitboard(["a3", "b1", "c3", "d3"]): 1.0 / 4, + }, + ) + else: + target_is_occupied = world.post_selection[world["ancilla_d2_0"]] + if not target_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b1", "c2", "d3"]): 1.0 / 2, + locations_to_bitboard(["a2", "b1", "c3", "d3"]): 1.0 / 2, + }, + ) + else: + path_is_blocked = world.post_selection[world["ancilla_c2_0"]] + if path_is_blocked: + assert_samples_in( + board, {locations_to_bitboard(["a2", "b1", "c2", "d2"]): 1.0} + ) + else: + # successful fire + assert_samples_in( + board, {locations_to_bitboard(["b1", "c3", "d2"]): 1.0} + ) + + +def test_cannon_fire_multiple_quantum_pieces(): + """There are one classical piece and multiple quantum pieces in path.""" + board = set_board(["a1", "b1", "c1", "d1", "e1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + SplitJump()(world["e1"], world["e2"], world["e3"]) + + CannonFire(["b1"], ["c2", "d2"])(world["a2"], world["e2"]) + + # We check the ancilla to learn if the fire was applied or not. + source_is_occupied = world.post_selection[world["ancilla_a2_0"]] + if not source_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a3", "b1", "c2", "d2", "e2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c2", "d2", "e3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c2", "d3", "e2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c2", "d3", "e3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c3", "d2", "e2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c3", "d2", "e3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c3", "d3", "e2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b1", "c3", "d3", "e3"]): 1.0 / 8, + }, + ) + else: + target_is_occupied = world.post_selection[world["ancilla_e2_0"]] + if not target_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b1", "c2", "d2", "e3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b1", "c2", "d3", "e3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b1", "c3", "d2", "e3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b1", "c3", "d3", "e3"]): 1.0 / 4, + }, + ) + else: + captured = world.post_selection[world["ancilla_ancilla_a2e2_0_0"]] + if not captured: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b1", "c2", "d2", "e2"]): 1.0 / 3, + locations_to_bitboard(["a2", "b1", "c2", "d3", "e2"]): 1.0 / 3, + locations_to_bitboard(["a2", "b1", "c3", "d2", "e2"]): 1.0 / 3, + }, + ) + else: + # successful fire + assert_samples_in( + board, {locations_to_bitboard(["b1", "c3", "d3", "e2"]): 1.0} + ) + + +def test_cannon_fire_no_classical_piece_in_path(): + """There is no classical piece in path.""" + board = set_board(["a1", "b1", "c1", "d1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) + + CannonFire([], ["b2", "c2"])(world["a2"], world["d2"]) + + # We check the ancilla to learn if the fire was applied or not. + source_is_occupied = world.post_selection[world["ancilla_a2_0"]] + if not source_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 8, + }, + ) + else: + target_is_occupied = world.post_selection[world["ancilla_d2_0"]] + if not target_is_occupied: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1.0 / 4, + locations_to_bitboard(["a2", "b3", "c3", "d3"]): 1.0 / 4, + }, + ) + else: + only_b2_is_occupied_in_path = world.post_selection[ + world["ancilla_ancilla_b2_0_0"] + ] + if only_b2_is_occupied_in_path: + # successful fire + assert_samples_in( + board, {locations_to_bitboard(["b2", "c3", "d2"]): 1.0} + ) + else: + only_c2_is_occupied_in_path = world.post_selection[ + world["ancilla_ancilla_c2_0_0"] + ] + if only_c2_is_occupied_in_path: + # successful fire + assert_samples_in( + board, {locations_to_bitboard(["b3", "c2", "d2"]): 1.0} + ) + else: + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 2, + locations_to_bitboard(["a2", "b3", "c3", "d2"]): 1.0 / 2, + }, + )