From ffaa6a736547e450a0d45ea0c6061953a8fd0971 Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Thu, 12 Oct 2023 15:34:31 -0700 Subject: [PATCH 01/31] q moves --- .../quantum_chinese_chess/__init__.py | 7 +- .../examples/quantum_chinese_chess/chess.py | 432 +++++++++++++++ .../examples/quantum_chinese_chess/move.py | 246 +++++++++ .../quantum_chinese_chess/move_test.py | 74 +++ .../examples/quantum_chinese_chess/piece.py | 19 + .../examples/quantum_chinese_chess/piece1.py | 44 ++ .../quantum_chinese_chess/quantum_world.py | 519 ++++++++++++++++++ 7 files changed, 1337 insertions(+), 4 deletions(-) create mode 100644 unitary/examples/quantum_chinese_chess/chess.py create mode 100644 unitary/examples/quantum_chinese_chess/move.py create mode 100644 unitary/examples/quantum_chinese_chess/move_test.py create mode 100644 unitary/examples/quantum_chinese_chess/piece1.py create mode 100644 unitary/examples/quantum_chinese_chess/quantum_world.py diff --git a/unitary/examples/quantum_chinese_chess/__init__.py b/unitary/examples/quantum_chinese_chess/__init__.py index 4d904923..64dd7178 100644 --- a/unitary/examples/quantum_chinese_chess/__init__.py +++ b/unitary/examples/quantum_chinese_chess/__init__.py @@ -1,15 +1,14 @@ -# Copyright 2023 The Unitary Authors +# Copyright 2022 Google # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -"""Package for the quantum Chinese chess game.""" +# diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py new file mode 100644 index 00000000..0990b991 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -0,0 +1,432 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Tuple, List +from unitary.examples.quantum_chinese_chess.board import Board +from unitary.examples.quantum_chinese_chess.enums import ( + Language, + GameState, + Type, + Color, + MoveType, + MoveVariant, +) +from unitary.examples.quantum_chinese_chess.move import Move, Jump + +# List of accepable commands. +_HELP_TEXT = """ + Each location on the board is represented by two characters [abcdefghi][0-9], i.e. from a0 to i9. You may input (s=source, t=target) + - s1t1 to do a slide move, e.g. "a1a4"; + - s1^t1t2 to do a split move, e.g. "a1^b1a2"; + - s1s2^t1 to do a merge move, e.g. "b1a2^a1"; + Other commands: + - "exit" to quit + - "help": to see this message again +""" + +_WELCOME_MESSAGE = """ + Welcome to Quantum Chinese Chess! +""" + + +class QuantumChineseChess: + """A class that implements Quantum Chinese Chess using the unitary API.""" + + def print_welcome(self) -> None: + """Prints the welcome message. Gets board language and players' name.""" + print(_WELCOME_MESSAGE) + print(_HELP_TEXT) + # TODO(): add the whole set of Chinese interface support. + lang = input( + "Switch to Chinese board characters? (y/n) (default to be English) " + ) + if lang.lower() == "y": + self.lang = Language.ZH + else: + self.lang = Language.EN + name_0 = input("Player 0's name (default to be Player_0): ") + self.players_name.append("Player_0" if len(name_0) == 0 else name_0) + name_1 = input("Player 1's name (default to be Player_1): ") + self.players_name.append("Player_1" if len(name_1) == 0 else name_1) + + def __init__(self): + self.players_name = [] + self.print_welcome() + self.board = Board.from_fen() + self.board.set_language(self.lang) + print(self.board) + self.game_state = GameState.CONTINUES + self.current_player = self.board.current_player + self.debug_level = 3 + + @staticmethod + def parse_input_string(str_to_parse: str) -> Tuple[List[str], List[str]]: + """Check if the input string could be turned into a valid move. + Returns the sources and targets if it is valid. + The input needs to be: + - s1t1 for slide/jump move; or + - s1^t1t2 for split moves; or + - s1s2^t1 for merge moves. + Examples: + 'a1a2' + 'b1^a3c3' + 'a3b1^c3' + """ + sources = None + targets = None + + if "^" in str_to_parse: + sources_str, targets_str = str_to_parse.split("^", maxsplit=1) + # The only two allowed cases here are s1^t1t2 and s1s2^t1. + if ( + str_to_parse.count("^") > 1 + or len(str_to_parse) != 7 + or len(sources_str) not in [2, 4] + ): + raise ValueError(f"Invalid sources/targets string {str_to_parse}.") + sources = [sources_str[i : i + 2] for i in range(0, len(sources_str), 2)] + targets = [targets_str[i : i + 2] for i in range(0, len(targets_str), 2)] + if len(sources) == 2: + if sources[0] == sources[1]: + raise ValueError("Two sources should not be the same.") + elif targets[0] == targets[1]: + raise ValueError("Two targets should not be the same.") + else: + # The only allowed case here is s1t1. + if len(str_to_parse) != 4: + raise ValueError(f"Invalid sources/targets string {str_to_parse}.") + sources = [str_to_parse[0:2]] + targets = [str_to_parse[2:4]] + if sources[0] == targets[0]: + raise ValueError("Source and target should not be the same.") + + # Make sure all the locations are valid. + for location in sources + targets: + if location[0].lower() not in "abcdefghi" or not location[1].isdigit(): + raise ValueError( + f"Invalid location string. Make sure they are from a0 to i9." + ) + return sources, targets + + def check_classical_rule( + self, source: str, target: str, classical_path_pieces: List[str] + ) -> None: + """Check if the proposed move satisfies classical rules, and raises ValueError if not.""" + source_piece = self.board.board[source] + target_piece = self.board.board[target] + # Check if the move is blocked by classical path piece. + if len(classical_path_pieces) > 0 and source_piece.type_ != Type.CANNON: + # The path is blocked by classical pieces. + raise ValueError("The path is blocked.") + + # Check if the target has classical piece of the same color. + if not target_piece.is_entangled and source_piece.color == target_piece.color: + raise ValueError( + "The target place has classical piece with the same color." + ) + + # Check if the move violates any classical rule. + x0 = ord(source[0]) + x1 = ord(target[0]) + dx = x1 - x0 + y0 = int(source[1]) + y1 = int(target[1]) + dy = y1 - y0 + + if source_piece.type_ == Type.ROOK: + if dx != 0 and dy != 0: + raise ValueError("ROOK cannot move like this.") + elif source_piece.type_ == Type.HORSE: + if not ((abs(dx) == 2 and abs(dy) == 1) or (abs(dx) == 1 and abs(dy) == 2)): + raise ValueError("HORSE cannot move like this.") + elif source_piece.type_ == Type.ELEPHANT: + if not (abs(dx) == 2 and abs(dy) == 2): + raise ValueError("ELEPHANT cannot move like this.") + if (source_piece.color == Color.RED and y1 < 5) or ( + source_piece.color == Color.BLACK and y1 > 4 + ): + raise ValueError( + "ELEPHANT cannot cross the river (i.e. the middle line)." + ) + elif source_piece.type_ == Type.ADVISOR: + if not (abs(dx) == 1 and abs(dy) == 1): + raise ValueError("ADVISOR cannot move like this.") + if ( + x1 > ord("f") + or x1 < ord("d") + or (source_piece.color == Color.RED and y1 < 7) + or (source_piece.color == Color.BLACK and y1 > 2) + ): + raise ValueError("ADVISOR cannot leave the palace.") + elif source_piece.type_ == Type.KING: + if abs(dx) + abs(dy) != 1: + raise ValueError("KING cannot move like this.") + if ( + x1 > ord("f") + or x1 < ord("d") + or (source_piece.color == Color.RED and y1 < 7) + or (source_piece.color == Color.BLACK and y1 > 2) + ): + raise ValueError("KING cannot leave the palace.") + elif source_piece.type_ == Type.CANNON: + if dx != 0 and dy != 0: + raise ValueError("CANNON cannot move like this.") + if len(classical_path_pieces) > 0: + if len(classical_path_pieces) > 1: + # Invalid cannon move, since there could only be at most one classical piece between + # the source (i.e. the cannon) and the target. + raise ValueError("CANNON cannot fire like this.") + elif source_piece.color == target_piece.color: + raise ValueError("CANNON cannot fire to a piece with same color.") + elif target_piece.color == Color.NA: + raise ValueError("CANNON cannot fire to an empty piece.") + elif source_piece.type_ == Type.PAWN: + if abs(dx) + abs(dy) != 1: + raise ValueError("PAWN cannot move like this.") + if source_piece.color == Color.RED: + if dy == 1: + raise ValueError("PAWN can not move backward.") + if y0 > 4 and dy != -1: + raise ValueError( + "PAWN can only go forward before crossing the river (i.e. the middle line)." + ) + else: + if dy == -1: + raise ValueError("PAWN can not move backward.") + if y0 <= 4 and dy != 1: + raise ValueError( + "PAWN can only go forward before crossing the river (i.e. the middle line)." + ) + + def classify_move( + self, + sources: List[str], + targets: List[str], + classical_path_pieces_0: List[str], + quantum_path_pieces_0: List[str], + classical_path_pieces_1: List[str], + quantum_path_pieces_1: List[str], + ) -> Tuple[MoveType, MoveVariant]: + """Determines the MoveType and MoveVariant.""" + move_type = MoveType.UNSPECIFIED_STANDARD + move_variant = MoveVariant.UNSPECIFIED + + source = self.board.board[sources[0]] + target = self.board.board[targets[0]] + + if len(sources) == 1 and len(targets) == 1: + if len(quantum_path_pieces_0) == 0: + if ( + len(classical_path_pieces_0) == 0 + and source.type_ == Type.CANNON + and target.color.value == 1 - source.color.value + ): + raise ValueError( + "CANNON could not fire/capture without a cannon platform." + ) + if not source.is_entangled and not target.is_entangled: + return MoveType.CLASSICAL, MoveVariant.UNSPECIFIED + else: + move_type = MoveType.JUMP + else: + move_type = MoveType.SLIDE + + if ( + move_type != MoveType.CLASSICAL + and source.type_ == Type.CANNON + and ( + len(classical_path_pieces_0) == 1 or len(quantum_path_pieces_0) > 0 + ) + ): + # By this time the classical cannon fire has been identified as CLASSICAL JUMP. + return MoveType.CANNON_FIRE, MoveVariant.CAPTURE + # Determine MoveVariant. + if target.color == Color.NA: + move_variant = MoveVariant.BASIC + # TODO(): such move could be a merge. Take care of such cases later. + elif target.color == source.color: + move_variant = MoveVariant.EXCLUDED + else: + move_variant = MoveVariant.CAPTURE + + elif len(sources) == 2: + source_1 = self.board.board[sources[1]] + if not source.is_entangled or not source_1.is_entangled: + raise ValueError( + "Both sources need to be in quantum state in order to merge." + ) + # TODO(): Currently we don't support merge + excluded/capture, or cannon_merge_fire + capture. Maybe add support later. + if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: + raise ValueError("Currently CANNON could not merge while fire.") + if target.type_ != Type.EMPTY: + raise ValueError("Currently we could only merge into an empty piece.") + if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: + move_type = MoveType.MERGE_JUMP + else: + move_type = MoveType.MERGE_SLIDE + move_variant = MoveVariant.BASIC + + elif len(targets) == 2: + target_1 = self.board.board[targets[1]] + # TODO(): Currently we don't support split + excluded/capture, or cannon_split_fire + capture. Maybee add support later. + if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: + raise ValueError("Currently CANNON could not split while fire.") + if target.type_ != Type.EMPTY or target_1.type_ != Type.EMPTY: + raise ValueError("Currently we could only split into empty pieces.") + if source.type_ == Type.KING: + # TODO(): Currently we don't support KING split. Maybe add support later. + raise ValueError("King split is not supported currently.") + if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: + move_type = MoveType.SPLIT_JUMP + else: + move_type = MoveType.SPLIT_SLIDE + move_variant = MoveVariant.BASIC + return move_type, move_variant + + def apply_move(self, str_to_parse: str) -> None: + """Check if the input string is valid. If it is, determine the move type and variant and return the move.""" + sources, targets = self.parse_input_string(str_to_parse) + + # Additional checks based on the current board. + for source in sources: + if self.board.board[source].type_ == Type.EMPTY: + raise ValueError("Could not move empty piece.") + if self.board.board[source].color.value != self.board.current_player: + raise ValueError("Could not move the other player's piece.") + source_0 = self.board.board[sources[0]] + target_0 = self.board.board[targets[0]] + if len(sources) == 2: + source_1 = self.board.board[sources[1]] + if source_0.type_ != source_1.type_: + raise ValueError("Two sources need to be the same type.") + if len(targets) == 2: + target_1 = self.board.board[targets[1]] + # TODO(): handle the case where a piece is split into the current piece and another piece, in which case two targets are different. + if target_0.type_ != target_1.type_: + raise ValueError("Two targets need to be the same type.") + if target_0.color != target_1.color: + raise ValueError("Two targets need to be the same color.") + + # Check if the first path satisfies the classical rule. + classical_pieces_0, quantum_pieces_0 = self.board.path_pieces( + sources[0], targets[0] + ) + self.check_classical_rule(sources[0], targets[0], classical_pieces_0) + + # Check if the second path (if exists) satisfies the classical rule. + classical_pieces_1 = None + quantum_pieces_1 = None + + if len(sources) == 2: + classical_pieces_1, quantum_pieces_1 = self.board.path_pieces( + sources[1], targets[0] + ) + self.check_classical_rule(sources[1], targets[0], classical_pieces_1) + elif len(targets) == 2: + classical_pieces_1, quantum_pieces_1 = self.board.path_pieces( + sources[0], targets[1] + ) + self.check_classical_rule(sources[0], targets[1], classical_pieces_1) + # Classify the move type and move variant. + move_type, move_variant = self.classify_move( + sources, + targets, + classical_pieces_0, + quantum_pieces_0, + classical_pieces_1, + quantum_pieces_1, + ) + + if move_type == MoveType.CLASSICAL: + if source_0.type_ == Type.KING: + # Update the locations of KING. + self.board.king_locations[self.current_player] = targets[0] + # TODO(): only make such prints for a certain debug level. + print(f"Updated king locations: {self.board.king_locations}.") + if target_0.type_ == Type.KING: + # King is captured, then the game is over. + self.game_state = GameState(self.current_player) + target_0.reset(source_0) + source_0.reset() + # TODO(): only make such prints for a certain debug level. + print("Classical move.") + elif move_type == MoveType.JUMP: + Jump(source_0, target_0, self.board).effect() + + def next_move(self) -> bool: + """Check if the player wants to exit or needs help message. Otherwise parse and apply the move. + Returns True if the move was made, otherwise returns False. + """ + input_str = input( + f"\nIt is {self.players_name[self.current_player]}'s turn to move: " + ) + if input_str.lower() == "help": + print(_HELP_TEXT) + elif input_str.lower() == "exit": + # The other player wins if the current player quits. + self.game_state = GameState(1 - self.current_player) + print("Exiting.") + else: + try: + # The move is success if no ValueError is raised. + self.apply_move(input_str.lower()) + return True + except ValueError as e: + print("Invalid move.") + print(e) + return False + + def game_over(self) -> None: + """Checks if the game is over, and update self.game_state accordingly.""" + if self.game_state != GameState.CONTINUES: + return + if self.board.flying_general_check(): + # If two KINGs are directly facing each other (i.e. in the same column) without any pieces in between, then the game ends. The other player wins. + self.game_state = GameState(1 - self.current_player) + return + # TODO(): add the following checks + # - If player 0 made N repeatd back-and_forth moves in a row. + + def play(self) -> None: + """The loop where each player takes turn to play.""" + while True: + move_success = self.next_move() + print(self.board) + if not move_success: + # Continue if the player does not quit. + if self.game_state == GameState.CONTINUES: + print("\nPlease re-enter your move.") + continue + # Check if the game is over. + self.game_over() + if self.game_state == GameState.CONTINUES: + # If the game continues, switch the player. + self.current_player = 1 - self.current_player + self.board.current_player = self.current_player + continue + elif self.game_state == GameState.RED_WINS: + print(f"{self.players_name[0]} wins! Game is over.") + elif self.game_state == GameState.BLACK_WINS: + print(f"{self.players_name[1]} wins! Game is over.") + elif self.game_state == GameState.DRAW: + print("Draw! Game is over.") + break + + +def main(): + game = QuantumChineseChess() + game.play() + + +if __name__ == "__main__": + main() diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py new file mode 100644 index 00000000..b79afd3a --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -0,0 +1,246 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional, List, Tuple, Iterator +import cirq +from unitary import alpha +from unitary.alpha.quantum_effect import QuantumEffect +from unitary.examples.quantum_chinese_chess.board import Board +from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type + + +def controlled_operation(gate, qubits, path_qubits, anti_qubits): + """Apply gate on qubits, controlled by path_qubits and + anti-controlled by anti_qubits. + """ + for qubit in anti_qubits: + yield cirq.X(qubit) + yield gate.on(*qubits).controlled_by(*path_qubits, *anti_qubits) + for qubit in anti_qubits: + yield cirq.X(qubit) + + +class Move(QuantumEffect): + """The base class of all chess moves.""" + + def __init__( + self, + source_0: Piece, + target_0: Piece, + board: Board, + move_type: MoveType, + move_variant: MoveVariant, + source_1: Piece = None, + target_1: Piece = None, + ): + self.source_0 = source_0 + self.source_1 = source_1 + self.target_0 = target_0 + self.target_1 = target_1 + self.move_type = move_type + self.move_variant = move_variant + self.board = board + + def __eq__(self, other): + return self.to_str(3) == other.to_str(3) + + def num_dimension(self) -> Optional[int]: + return 2 + + def _verify_objects(self, *objects): + # TODO(): add checks that apply to all move types + return + + def effect(self, *objects): + # TODO(): add effects according to move_type and move_variant + return + + def is_split_move(self) -> bool: + return self.target_1 is not None + + def is_merge_move(self) -> bool: + return self.source_1 is not None + + def to_str(self, verbose_level: int = 1) -> str: + """Constructs the string representation of the move. + According to the value of verbose_level: + - 1: only returns the move source(s) and target(s); + - 2: additionally returns the move type and variant; + - 3: additionally returns the source(s) and target(s) piece type and color. + """ + if verbose_level < 1: + return "" + + if self.is_split_move(): + move_str = [self.source_0.name + "^" + self.target_0.name + self.target_1.name] + elif self.is_merge_move(): + move_str = [self.source_0.name + self.source_1.name + "^" + self.target_0.name] + else: + move_str = [self.source_0.name + self.target_0.name] + + if verbose_level > 1: + move_str.append(self.move_type.name) + move_str.append(self.move_variant.name) + + if verbose_level > 2: + move_str.append( + self.source_0.color.name + + "_" + + self.source_0.type_.name + + "->" + + self.target_0.color.name + + "_" + + self.target_0.type_.name + ) + return ":".join(move_str) + + def __str__(self): + return self.to_str() + + +class Jump(Move): + def __init__( + self, + source_0: Piece, + target_0: Piece, + board: Board, + move_variant: MoveVariant, + ): + super().__init__(source_0, target_0, board, move_type=MoveType.JUMP, move_variant=move_variant) + + def effect(self, *objects) -> Iterator[cirq.Operation]: + # TODO(): currently pawn capture is a same as jump capture, while in quantum chess it's different, + # i.e. pawn would only move if the source is there, i.e. CNOT(t, s), and an entanglement could be + # created. This could be a general game setting, i.e. we could allow players to choose if they + # want the source piece to move (in case of capture) if the target piece is not there. + if self.move_variant == MoveVariant.CAPTURE: + source_is_occupied = self.board.board.pop(self.source_0) + if not source_is_occupied: + self.source_0.reset() + print("Jump move: source turns out to be empty.") + return + self.source_0.is_entangled = False + # TODO(): we should implement and do unhook instead of force_measurement. + if self.target_0.is_entangled: + self.board.board.force_measurement(self.target_0, 0) + self.target_0.reset() + elif move_variant == MoveVariant.EXCLUDED: + target_is_occupied = self.board.board.pop(self.target_0) + if target_is_occupied: + print("Jump move: target turns out to be occupied.") + self.target_0.is_entangled = False + return + self.target_0.reset() + alpha.PhasedMove().effect(self.source_0, self.target_0) + self.target_0.reset(self.source_0) + self.source_0.reset() + + +class SplitJump(Move): + def __init__(self, + source_0: Piece, + target_0: Piece, + target_1: Piece, + board: Board, + ): + super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, target_1 = target_1) + + def effect(self, *objects) -> Iterator[cirq.Operation]: + self.source_0.is_entangled() = True + alpha.PhasedSplit().effect(self.source_0, self.target_0, self.target_1) + self.target_0.reset(self.source_0) + self.target_1.reset(self.source_0) + self.source_0.reset() + + +class MergeJump(Move): + def __init__(self, + source_0: Piece, + source_1: Piece, + target_0: Piece, + board: Board, + ): + super().__init__(source_0, target_0, board, move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.BASIC, source_1) + + def effect(self, *objects) -> Iterator[cirq.Operation]: + yield cirq.ISWAP(self.source_0, self.target_0) ** -0.5 + yield cirq.ISWAP(self.source_0, self.target_0) ** -0.5 + yield cirq.ISWAP(self.source_1, self.target_0) ** -0.5 + self.target_0.reset(self.source_0) + # TODO(): double check if we should do the following reset(). + self.source_1.reset() + self.source_0.reset() + +class Slide(Move): + def __init__(self, + source_0: Piece, + target_0: Piece, + quantum_path_pieces_0: List[str], + board: Board, + move_variant:MoveVariant + ): + super().__init__(source_0, target_0, board, move_type=MoveType.SLIDE, move_variant=move_variant) + self.quantum_path_pieces_0 = [self.board.board[path] for path in quantum_path_pieces_0] + + def effect(self, *objects) -> Iterator[cirq.Operation]: + if move_variant == MoveVariant.EXCLUDED: + target_is_occupied = self.board.board.pop(self.target_0) + if target_is_occupied: + print("Slide move not applied: target turns out to be occupied.") + self.target_0.is_entangled = False + return + self.target_0.reset() + elif move_variant == MoveVariant.CAPTURE: + could_capture = False + if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: + could_capture = 1 - self.board.board.pop(quantum_path_pieces_0[0]) + else: + source_0.is_entangled = True + capture_ancilla = self.board.board._add_ancilla(f"{source_0.name}{target_0.name}") + alpha.quantum_if([source_0] + quantum_path_pieces_0).equals([1] + [0] * len(quantum_path_pieces_0)).apply(alpha.Flip())(capture_ancilla) + could_capture = self.board.board.pop(capture_ancilla) + if not could_capture: + # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player + # could try to do this move again. Is this desired? + print("Slide move not applied: either the source turns out be empty, or the path turns out to be blocked.") + return + self.target_0.reset(self.source_0) + self.source_0.reset() + alpha.PhasedMove().effect(self.source_0, self.target_0) + + + + + +class SplitSlide(Move): + def __init__(self, + source_0: Piece, + target_0: Piece, + target_1: Piece, + quantum_path_pieces_0: List[str], + quantum_path_pieces_1: List[str], + board: Board, + ): + super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_SLIDE, move_variant=MoveVariant.BASIC, target_1=target_1) + self.quantum_path_pieces_0 = [self.board.board[path] for path in quantum_path_pieces_0 if path != target_1.name] + self.quantum_path_pieces_1 = [self.board.board[path] for path in quantum_path_pieces_1 if path != target_0.name] + + def effect(self, *objects) -> Iterator[cirq.Operation]: + if len(self.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) and + # into classify_move(). + SplitJump(self.source_0, self.target_0, self.target_1, self.board).effect() + return + diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py new file mode 100644 index 00000000..c90b201a --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -0,0 +1,74 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unitary.examples.quantum_chinese_chess.move import Move +from unitary.examples.quantum_chinese_chess.board import Board +from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant +import pytest +from string import ascii_lowercase, digits + + +def global_names(): + global board + board = Board.from_fen() + for col in ascii_lowercase[:9]: + for row in digits: + globals()[f"{col}{row}"] = board.board[f"{col}{row}"] + + +def test_move_eq(): + global_names() + move1 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) + move2 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) + move3 = Move(a1, b2, board, MoveType.JUMP, MoveVariant.CAPTURE) + move4 = Move(a1, b2, board, MoveType.MERGE_SLIDE, MoveVariant.CAPTURE, c1) + + assert move1 == move2 + assert move1 != move3 + assert move1 != move4 + + +def test_move_type(): + # TODO(): change to real senarios + move1 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) + assert move1.is_split_move() == False + assert move1.is_merge_move() + + move2 = Move(a1, b2, board, MoveType.SPLIT_JUMP, MoveVariant.BASIC, target_1=c1) + assert move2.is_split_move() + assert move2.is_merge_move() == False + + move3 = Move(a1, b2, board, MoveType.SLIDE, MoveVariant.CAPTURE) + assert move3.is_split_move() == False + assert move3.is_merge_move() == False + + +def test_to_str(): + # TODO(): change to real scenarios + move1 = Move(a0, a6, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) + assert move1.to_str(0) == "" + assert move1.to_str(1) == "a0c1^a6" + assert move1.to_str(2) == "a0c1^a6:MERGE_JUMP:CAPTURE" + assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:BLACK_ROOK->RED_PAWN" + + move2 = Move(a0, b3, board, MoveType.SPLIT_JUMP, MoveVariant.BASIC, target_1=c1) + assert move2.to_str(0) == "" + assert move2.to_str(1) == "a0^b3c1" + assert move2.to_str(2) == "a0^b3c1:SPLIT_JUMP:BASIC" + assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:BLACK_ROOK->NA_EMPTY" + + move3 = Move(a0, a6, board, MoveType.SLIDE, MoveVariant.CAPTURE) + assert move3.to_str(0) == "" + assert move3.to_str(1) == "a0a6" + assert move3.to_str(2) == "a0a6:SLIDE:CAPTURE" + assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:BLACK_ROOK->RED_PAWN" diff --git a/unitary/examples/quantum_chinese_chess/piece.py b/unitary/examples/quantum_chinese_chess/piece.py index 6335c530..628ef959 100644 --- a/unitary/examples/quantum_chinese_chess/piece.py +++ b/unitary/examples/quantum_chinese_chess/piece.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from unitary.alpha import QuantumObject from unitary.examples.quantum_chinese_chess.enums import ( SquareState, @@ -25,9 +26,27 @@ def __init__(self, name: str, state: SquareState, type_: Type, color: Color): QuantumObject.__init__(self, name, state) self.type_ = type_ self.color = color + # TODO(): maybe modify QuantumObject to allow it to: + # - not create qubit at initialization; + # - add qubit and set it to the current classical state when needed. + self.is_entangled = False def symbol(self, lang: Language = Language.EN) -> str: + """Returns the symbol of this piece according to its type, color and language.""" return Type.symbol(self.type_, self.color, lang) def __str__(self): return self.symbol() + + def reset(self, piece: "Piece" = None) -> None: + """Modifies the classical attributes of the piece. + If piece is provided, then its type_ and color is copied, otherwise set the current piece to be an empty piece. + """ + if piece is not None: + self.type_ = piece.type_ + self.color = piece.color + self.is_entangled = piece.is_entangled + else: + self.type_ = Type.EMPTY + self.color = Color.NA + self.is_entangled = False diff --git a/unitary/examples/quantum_chinese_chess/piece1.py b/unitary/examples/quantum_chinese_chess/piece1.py new file mode 100644 index 00000000..130afab2 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/piece1.py @@ -0,0 +1,44 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unitary.alpha import QuantumObject +from unitary.examples.quantum_chinese_chess.enums import ( + SquareState, + Language, + Color, + Type, +) + + +class Piece: + def __init__(self, type_: Type, color: Color): + self.type_ = type_ + self.color = color + self.is_entangled = False + + def symbol(self, lang: Language = Language.EN) -> str: + return Type.symbol(self.type_, self.color, lang) + + def __str__(self): + return self.symbol() + + def clear(self): + self.type_ = Type.EMPTY + self.color = Color.NA + self.is_entangled = False + + +class QuantumPiece(Piece, QuantumObject): + def __init__(self, name: str, state: SquareState, type_: Type, color: Color): + QuantumObject.__init__(self, name, state) + Piece.__init__(self, name, state, type_, color) diff --git a/unitary/examples/quantum_chinese_chess/quantum_world.py b/unitary/examples/quantum_chinese_chess/quantum_world.py new file mode 100644 index 00000000..82a305d0 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/quantum_world.py @@ -0,0 +1,519 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy +import enum +from typing import cast, Dict, Iterable, List, Optional, Sequence, Set, Tuple, 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: + """A collection of `QuantumObject`s with effects. + + This object represents an entire state of a quantum game. + This includes all the `QuantumObjects` as well as the effects + that have been applied to them. It also includes all + criteria of measurement so that repetitions of the circuit + will be the same even if some of the quantum objects have + already been measured, + + This object also has a history so that effects can be undone. + + 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, + sampler=cirq.Simulator(), + compile_to_qubits: bool = False, + ): + self.clear() + self.sampler = sampler + self.use_sparse = isinstance(sampler, SparseSimulator) + self.compile_to_qubits = compile_to_qubits + + if isinstance(objects, QuantumObject): + objects = [objects] + if objects is not None: + for obj in objects: + self.add_object(obj) + + def clear(self) -> None: + """Removes all objects and effects from this QuantumWorld. + + This will reset the QuantumWorld to an empty state. + """ + self.circuit = cirq.Circuit() + self.effect_history: List[Tuple[cirq.Circuit, Dict[QuantumObject, int]]] = [] + self.object_name_dict: Dict[str, QuantumObject] = {} + self.ancilla_names: Set[str] = 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] = {} + + def copy(self) -> "QuantumWorld": + new_objects = [] + new_post_selection: Dict[QuantumObject, int] = {} + for obj in self.object_name_dict.values(): + new_obj = copy.copy(obj) + new_objects.append(new_obj) + if obj in self.post_selection: + new_post_selection[new_obj] = self.post_selection[obj] + new_world = self.__class__( + objects=new_objects, + sampler=self.sampler, + compile_to_qubits=self.compile_to_qubits, + ) + new_world.circuit = self.circuit.copy() + new_world.ancilla_names = self.ancilla_names.copy() + new_world.effect_history = [ + (circuit.copy(), copy.copy(post_selection)) + for circuit, post_selection in self.effect_history + ] + new_world.post_selection = new_post_selection + return new_world + + def add_object(self, obj: QuantumObject): + """Adds a QuantumObject to the QuantumWorld. + + Raises: + ValueError: if an object with the same name has + already been added to the world. + """ + if obj.name in self.object_name_dict: + 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: + 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 + def objects(self) -> List[QuantumObject]: + return list(self.object_name_dict.values()) + + @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 + ] + + def get_object_by_name(self, name: str) -> Optional[QuantumObject]: + """Returns the object with the given name. + + If the object with that name does not exist in this QuantumWorld, + the function returns None. + """ + return self.object_name_dict.get(name) + + def combine_with(self, other_world: "QuantumWorld"): + """Combines all the objects from the specified world into this one. + + This will add all the objects as well as all the effects into the + current world. The passed in world then becomes unusable. + + Note that the effect history is cleared when this function is called, + so previous effects cannot be undone. + """ + my_keys = set(self.object_name_dict.keys()) + other_keys = set(other_world.object_name_dict.keys()) + if my_keys.intersection(other_keys): + raise ValueError("Cannot combine two worlds with overlapping object keys") + if self.use_sparse != other_world.use_sparse: + raise ValueError("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 + self.effect_history.clear() + # 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. + + 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 + or isinstance(op, PostSelectOperation) + or op.gate is cirq.X + ): + strategy = cirq.InsertStrategy.EARLIEST + else: + strategy = cirq.InsertStrategy.NEW + + if self.compile_to_qubits: + op = self._compile_op(op) + + self.circuit.append(op, strategy=strategy) + + 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(#77): 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 = qid_shape[0] + if qudit_dim == 2: + return op + num_qudits = len(qid_shape) + compiled_qubits = [] + 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, + 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.""" + self.effect_history.append( + (self.circuit.copy(), copy.copy(self.post_selection)) + ) + for op in op_list: + self._append_op(op) + + def undo_last_effect(self): + """Restores the `QuantumWorld` to the state before the last effect. + + Note that pop() is considered to be an effect for the purposes + of this call. + + Raises: + IndexError if there are no effects in the history. + """ + if not self.effect_history: + raise IndexError("No effects to undo") + self.circuit, self.post_selection = self.effect_history.pop() + + def _suggest_num_reps(self, sample_size: int) -> int: + """Guess the number of raw samples needed to get sample_size results. + Assume that each post-selection is about 50/50. + Noise and error mitigation will discard reps, so increase the total + number of repetitions to compensate. + """ + if self.use_sparse: + return sample_size + if len(self.post_selection) >= 1: + sample_size <<= len(self.post_selection) + 1 + if sample_size < 100: + sample_size = 100 + return sample_size + + def _interpret_result(self, result: Union[int, Iterable[int]]) -> 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. + return cirq.big_endian_bits_to_int(result) + if isinstance(result, Iterable): + # If it is a single-element iterable, return the first element. + result_list = list(result) + if len(result_list) != 1: + raise ValueError( + f"Cannot interpret a multivalued iterable {result} as a " + "single result for a non-compiled world." + ) + return result_list[0] + return result + + def force_measurement( + self, obj: QuantumObject, result: Union[enum.Enum, int] + ) -> None: + """Measures a QuantumObject with a defined outcome. + + This function will move the qubit to an ancilla and set + a post-selection criteria on it in order to force it + 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) + # 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.update( + {*zip(obj_qubits, new_obj_qubits), *zip(new_obj_qubits, obj_qubits)} + ) + + self.circuit = self.circuit.transform_qubits( + 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 + if self.use_sparse: + self._append_op(PostSelectOperation(new_obj.qubit, post_selection)) + + def peek( + self, + objects: Optional[Sequence[Union[QuantumObject, str]]] = None, + count: int = 1, + convert_to_enum: bool = True, + _existing_list: Optional[List[List[Union[enum.Enum, int]]]] = None, + _num_reps: Optional[int] = None, + ) -> List[List[Union[enum.Enum, int]]]: + """Measures the state of the system 'non-destructively'. + + This function will measure the state of each object. + It will _not_ modify the circuit of the QuantumWorld. + + Returns: + A list of measurement results. The length of the list will be + equal to the count parameter. Each element will be a list + of measurement results for each object. + """ + if _num_reps is None: + num_reps = self._suggest_num_reps(count) + else: + if _num_reps > 1e6: + raise RecursionError( + f"Count {count} reached without sufficient results. " + "Likely post-selection error" + ) + num_reps = _num_reps + + measure_circuit = self.circuit.copy() + if objects is None: + quantum_objects = self.public_objects + else: + quantum_objects = [ + self[obj_or_str] if isinstance(obj_or_str, str) else obj_or_str + for obj_or_str in objects + ] + measure_set = set(quantum_objects) + measure_set.update(self.post_selection.keys()) + 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 + rtn_list = _existing_list or [] + for rep in range(num_reps): + post_selected = True + for obj in self.post_selection.keys(): + result = self._interpret_result(results.measurements[obj.name][rep]) + if result != self.post_selection[obj]: + post_selected = False + break + if post_selected: + # All post-selections are satisfied. + rtn_list.append( + [ + self._interpret_result(results.measurements[obj.name][rep]) + for obj in quantum_objects + ] + ) + if len(rtn_list) == count: + break + if len(rtn_list) < count: + # We post-selected too much, get more reps + return self.peek( + quantum_objects, + count, + convert_to_enum, + rtn_list, + _num_reps=num_reps * 10, + ) + + if convert_to_enum: + rtn_list = [ + [quantum_objects[idx].enum_type(meas) for idx, meas in enumerate(res)] + for res in rtn_list + ] + + return rtn_list + + def pop( + self, + objects: Optional[Sequence[Union[QuantumObject, str]]] = None, + convert_to_enum: bool = True, + ) -> List[Union[enum.Enum, int]]: + """Peek the given objects (or all public objects if none is supplied) once and force measure them + with the peeked result. + + Returns: + A list of measurement results with one for each object. + """ + self.effect_history.append( + (self.circuit.copy(), copy.copy(self.post_selection)) + ) + if objects is None: + quantum_objects = self.public_objects + else: + quantum_objects = [ + self[obj_or_str] if isinstance(obj_or_str, str) else obj_or_str + for obj_or_str in objects + ] + results = self.peek(quantum_objects, convert_to_enum=convert_to_enum) + for idx, result in enumerate(results[0]): + self.force_measurement(quantum_objects[idx], result) + + return results[0] + + def get_histogram( + self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 + ) -> List[Dict[int, int]]: + """Creates histogram based on measurements (peeks) carried out. + + Parameters: + objects: List of QuantumObjects + count: Number of measurements + + Returns: + A list with one element for each object. Each element contains a dictionary with + counts for each state of the given object. + """ + if not objects: + objects = self.public_objects + peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) + histogram = [] + for obj in objects: + histogram.append({state: 0 for state in range(obj.num_states)}) + for result in peek_results: + for idx in range(len(objects)): + histogram[idx][cast(int, result[idx])] += 1 + return histogram + + def get_probabilities( + self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 + ) -> List[Dict[int, float]]: + """Calculates the probabilities based on measurements (peeks) carried out. + + Parameters: + objects: List of QuantumObjects + count: Number of measurements + + Returns: + A list with one element for each object. Each element contains a dictionary with + the probability for each state of the given object. + """ + histogram = self.get_histogram(objects=objects, count=count) + probabilities = [] + for obj_hist in histogram: + probabilities.append( + {state: obj_hist[state] / count for state in range(len(obj_hist))} + ) + return probabilities + + def get_binary_probabilities( + self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 + ) -> List[float]: + """Calculates the total probabilities for all non-zero states + based on measurements (peeks) carried out. + + Parameters: + objects: List of QuantumObjects + count: Number of measurements + + Returns: + A list with one element for each object which contains + the probability for the event state!=0. Which is the same as + 1.0-Probability(state==0). + """ + full_probs = self.get_probabilities(objects=objects, count=count) + binary_probs = [] + for one_probs in full_probs: + binary_probs.append(1 - one_probs[0]) + return binary_probs + + def __getitem__(self, name: str) -> QuantumObject: + quantum_object = self.object_name_dict.get(name, None) + if not quantum_object: + raise KeyError(f"{name} did not exist in this world.") + return quantum_object From 16059e8088edfa8b9e69d59a43aeca3f4518587c Mon Sep 17 00:00:00 2001 From: madcpf Date: Fri, 13 Oct 2023 12:20:51 -0700 Subject: [PATCH 02/31] update --- .../examples/quantum_chinese_chess/chess.py | 4 +- .../examples/quantum_chinese_chess/move.py | 203 ++++++++++++------ .../quantum_chinese_chess/quantum_world.py | 4 +- 3 files changed, 139 insertions(+), 72 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 0990b991..5c40452f 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -361,7 +361,9 @@ def apply_move(self, str_to_parse: str) -> None: # TODO(): only make such prints for a certain debug level. print("Classical move.") elif move_type == MoveType.JUMP: - Jump(source_0, target_0, self.board).effect() + # TODO(): maybe we could refactor to do + # Jump()(source_0, target_0) + Jump(move_variant).effect(source_0, target_0) def next_move(self) -> bool: """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 b79afd3a..3bfae2f3 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -109,138 +109,203 @@ def __str__(self): return self.to_str() -class Jump(Move): +class Jump(QuantumEffect): def __init__( self, - source_0: Piece, - target_0: Piece, - board: Board, + # source_0: Piece, + # target_0: Piece, + # board: Board, move_variant: MoveVariant, ): - super().__init__(source_0, target_0, board, move_type=MoveType.JUMP, move_variant=move_variant) + # super().__init__(source_0, target_0, board, move_type=MoveType.JUMP, move_variant=move_variant) + 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]: # TODO(): currently pawn capture is a same as jump capture, while in quantum chess it's different, # i.e. pawn would only move if the source is there, i.e. CNOT(t, s), and an entanglement could be # created. This could be a general game setting, i.e. we could allow players to choose if they # want the source piece to move (in case of capture) if the target piece is not there. + source_0, target_0 = objects + world = source_0.world if self.move_variant == MoveVariant.CAPTURE: - source_is_occupied = self.board.board.pop(self.source_0) + source_is_occupied = world.pop(source_0) if not source_is_occupied: - self.source_0.reset() + source_0.reset() print("Jump move: source turns out to be empty.") return - self.source_0.is_entangled = False - # TODO(): we should implement and do unhook instead of force_measurement. - if self.target_0.is_entangled: - self.board.board.force_measurement(self.target_0, 0) - self.target_0.reset() - elif move_variant == MoveVariant.EXCLUDED: - target_is_occupied = self.board.board.pop(self.target_0) + source_0.is_entangled = False + # TODO(): we should implement and do unhook instead of force_measurement, + # since there could be cases where target could be almost |1>. + if target_0.is_entangled: + world.force_measurement(target_0, 0) + target_0.reset() + elif self.move_variant == MoveVariant.EXCLUDED: + target_is_occupied = world.pop(target_0) if target_is_occupied: print("Jump move: target turns out to be occupied.") - self.target_0.is_entangled = False + target_0.is_entangled = False return - self.target_0.reset() - alpha.PhasedMove().effect(self.source_0, self.target_0) - self.target_0.reset(self.source_0) - self.source_0.reset() + target_0.reset() + alpha.PhasedMove().effect(source_0, target_0) + target_0.reset(source_0) + source_0.reset() class SplitJump(Move): def __init__(self, - source_0: Piece, - target_0: Piece, - target_1: Piece, - board: Board, + # source_0: Piece, + # target_0: Piece, + # target_1: Piece, + # board: Board, ): - super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, target_1 = target_1) + # super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, target_1 = target_1) + pass + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 3 def effect(self, *objects) -> Iterator[cirq.Operation]: - self.source_0.is_entangled() = True - alpha.PhasedSplit().effect(self.source_0, self.target_0, self.target_1) - self.target_0.reset(self.source_0) - self.target_1.reset(self.source_0) - self.source_0.reset() + source_0, target_0, target_1 = objects + source_0.is_entangled() = True + alpha.PhasedSplit().effect(source_0, target_0, target_1) + # Pass the classical properties of the source piece to the target pieces. + target_0.reset(source_0) + target_1.reset(source_0) + source_0.reset() class MergeJump(Move): def __init__(self, - source_0: Piece, - source_1: Piece, - target_0: Piece, - board: Board, + # source_0: Piece, + # source_1: Piece, + # target_0: Piece, + # board: Board, ): - super().__init__(source_0, target_0, board, move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.BASIC, source_1) + # super().__init__(source_0, target_0, board, move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.BASIC, source_1) + pass + + def num_dimension(self) -> Optional[int]: + return 2 + + def num_objects(self) -> Optional[int]: + return 3 def effect(self, *objects) -> Iterator[cirq.Operation]: - yield cirq.ISWAP(self.source_0, self.target_0) ** -0.5 - yield cirq.ISWAP(self.source_0, self.target_0) ** -0.5 - yield cirq.ISWAP(self.source_1, self.target_0) ** -0.5 - self.target_0.reset(self.source_0) + source_0, source_1, target_0 = objects + yield cirq.ISWAP(source_0, target_0) ** -0.5 + yield cirq.ISWAP(source_0, target_0) ** -0.5 + yield cirq.ISWAP(source_1, target_0) ** -0.5 + # Pass the classical properties of the source pieces to the target piece. + target_0.reset(source_0) # TODO(): double check if we should do the following reset(). - self.source_1.reset() - self.source_0.reset() + source_1.reset() + source_0.reset() + class Slide(Move): def __init__(self, - source_0: Piece, - target_0: Piece, + # source_0: Piece, + # target_0: Piece, quantum_path_pieces_0: List[str], - board: Board, + # board: Board, move_variant:MoveVariant ): - super().__init__(source_0, target_0, board, move_type=MoveType.SLIDE, move_variant=move_variant) - self.quantum_path_pieces_0 = [self.board.board[path] for path in quantum_path_pieces_0] + # super().__init__(source_0, target_0, board, move_type=MoveType.SLIDE, move_variant=move_variant) + 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]: - if move_variant == MoveVariant.EXCLUDED: - target_is_occupied = self.board.board.pop(self.target_0) + 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) if target_is_occupied: print("Slide move not applied: target turns out to be occupied.") - self.target_0.is_entangled = False + target_0.is_entangled = False return - self.target_0.reset() - elif move_variant == MoveVariant.CAPTURE: + # 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: - could_capture = 1 - self.board.board.pop(quantum_path_pieces_0[0]) + if not world.pop(quantum_path_pieces_0[0]): + quantum_path_pieces_0[0].reset() + could_capture = True else: source_0.is_entangled = True - capture_ancilla = self.board.board._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if([source_0] + quantum_path_pieces_0).equals([1] + [0] * len(quantum_path_pieces_0)).apply(alpha.Flip())(capture_ancilla) - could_capture = self.board.board.pop(capture_ancilla) + capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + alpha.quantum_if([source_0] + quantum_path_pieces_0).equals([1] + [0] * len(quantum_path_pieces_0)).apply(alpha.Flip()).effect(capture_ancilla) + could_capture = world.pop(capture_ancilla) if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? print("Slide move not applied: either the source turns out be empty, or the path turns out to be blocked.") return - self.target_0.reset(self.source_0) - self.source_0.reset() - alpha.PhasedMove().effect(self.source_0, self.target_0) - - - + # Apply the capture. + # TODO(): we should implement and do unhook instead of force_measurement, + # since there are cases where target could be |1>. + if target_0.is_entangled: + world.force_measurement(target_0, 0) + target_0.reset() + alpha.PhasedMove().effect(source_0, target_0) + # Pass the classical properties of 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() + # For BASIC or EXCLUDED cases + source_0.is_entangled = True + alpha.quantum_if(quantum_path_pieces_0).equals([0] * len(quantum_path_pieces_0)).apply(alpha.PhasedMove()).effect(source_0, target_0) + # Copy the classical properties of the source piece to the target piece. + target_0.reset(source_0) class SplitSlide(Move): def __init__(self, - source_0: Piece, - target_0: Piece, - target_1: Piece, + # source_0: Piece, + # target_0: Piece, + # target_1: Piece, quantum_path_pieces_0: List[str], quantum_path_pieces_1: List[str], - board: Board, + # board: Board, ): - super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_SLIDE, move_variant=MoveVariant.BASIC, target_1=target_1) - self.quantum_path_pieces_0 = [self.board.board[path] for path in quantum_path_pieces_0 if path != target_1.name] - self.quantum_path_pieces_1 = [self.board.board[path] for path in quantum_path_pieces_1 if path != target_0.name] + # super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_SLIDE, move_variant=MoveVariant.BASIC, target_1=target_1) + 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]: - if len(self.quantum_path_pieces_0) ==0 and len(self.quantum_path_pieces_1) == 0: + source_0, target_0, target_1 = objects + world = source_0.world + 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] + 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) and # into classify_move(). - SplitJump(self.source_0, self.target_0, self.target_1, self.board).effect() + SplitJump().effect(source_0, target_0, target_1) return diff --git a/unitary/examples/quantum_chinese_chess/quantum_world.py b/unitary/examples/quantum_chinese_chess/quantum_world.py index 82a305d0..7d6b3340 100644 --- a/unitary/examples/quantum_chinese_chess/quantum_world.py +++ b/unitary/examples/quantum_chinese_chess/quantum_world.py @@ -424,11 +424,11 @@ def pop( objects: Optional[Sequence[Union[QuantumObject, str]]] = None, convert_to_enum: bool = True, ) -> List[Union[enum.Enum, int]]: - """Peek the given objects (or all public objects if none is supplied) once and force measure them + """Peek the given objects (or all public objects if none is supplied) once and force measure them with the peeked result. Returns: - A list of measurement results with one for each object. + A list of measurement results with one for each object. """ self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection)) From 3fd0e68ef3408e208bb10f76ad67f56dd04125a0 Mon Sep 17 00:00:00 2001 From: madcpf Date: Fri, 13 Oct 2023 16:28:00 -0700 Subject: [PATCH 03/31] update flying_general_check --- .../examples/quantum_chinese_chess/chess.py | 18 +- .../examples/quantum_chinese_chess/move.py | 276 +++++++++++++++--- 2 files changed, 258 insertions(+), 36 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 5c40452f..3a296d6e 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -361,9 +361,19 @@ def apply_move(self, str_to_parse: str) -> None: # TODO(): only make such prints for a certain debug level. print("Classical move.") elif move_type == MoveType.JUMP: - # TODO(): maybe we could refactor to do - # Jump()(source_0, target_0) - Jump(move_variant).effect(source_0, target_0) + Jump(move_variant)(source_0, target_0) + 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) -> bool: """Check if the player wants to exit or needs help message. Otherwise parse and apply the move. @@ -394,6 +404,8 @@ def game_over(self) -> None: return if self.board.flying_general_check(): # If two KINGs are directly facing each other (i.e. in the same column) without any pieces in between, then the game ends. The other player wins. + print("==== FLYING GENERAL ! ====") + print(self.board) self.game_state = GameState(1 - self.current_player) return # TODO(): add the following checks diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 3bfae2f3..9f829474 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -20,17 +20,6 @@ from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type -def controlled_operation(gate, qubits, path_qubits, anti_qubits): - """Apply gate on qubits, controlled by path_qubits and - anti-controlled by anti_qubits. - """ - for qubit in anti_qubits: - yield cirq.X(qubit) - yield gate.on(*qubits).controlled_by(*path_qubits, *anti_qubits) - for qubit in anti_qubits: - yield cirq.X(qubit) - - class Move(QuantumEffect): """The base class of all chess moves.""" @@ -83,9 +72,13 @@ def to_str(self, verbose_level: int = 1) -> str: return "" if self.is_split_move(): - move_str = [self.source_0.name + "^" + self.target_0.name + self.target_1.name] + move_str = [ + self.source_0.name + "^" + self.target_0.name + self.target_1.name + ] elif self.is_merge_move(): - move_str = [self.source_0.name + self.source_1.name + "^" + self.target_0.name] + move_str = [ + self.source_0.name + self.source_1.name + "^" + self.target_0.name + ] else: move_str = [self.source_0.name + self.target_0.name] @@ -128,8 +121,8 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: # TODO(): currently pawn capture is a same as jump capture, while in quantum chess it's different, - # i.e. pawn would only move if the source is there, i.e. CNOT(t, s), and an entanglement could be - # created. This could be a general game setting, i.e. we could allow players to choose if they + # i.e. pawn would only move if the source is there, i.e. CNOT(t, s), and an entanglement could be + # created. This could be a general game setting, i.e. we could allow players to choose if they # want the source piece to move (in case of capture) if the target piece is not there. source_0, target_0 = objects world = source_0.world @@ -157,8 +150,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0.reset() -class SplitJump(Move): - def __init__(self, +class SplitJump(QuantumEffect): + def __init__( + self, # source_0: Piece, # target_0: Piece, # target_1: Piece, @@ -175,7 +169,7 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0, target_1 = objects - source_0.is_entangled() = True + source_0.is_entangled = True alpha.PhasedSplit().effect(source_0, target_0, target_1) # Pass the classical properties of the source piece to the target pieces. target_0.reset(source_0) @@ -183,8 +177,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0.reset() -class MergeJump(Move): - def __init__(self, +class MergeJump(QuantumEffect): + def __init__( + self, # source_0: Piece, # source_1: Piece, # target_0: Piece, @@ -211,13 +206,14 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0.reset() -class Slide(Move): - def __init__(self, +class Slide(QuantumEffect): + def __init__( + self, # source_0: Piece, # target_0: Piece, quantum_path_pieces_0: List[str], # board: Board, - move_variant:MoveVariant + move_variant: MoveVariant, ): # super().__init__(source_0, target_0, board, move_type=MoveType.SLIDE, move_variant=move_variant) self.quantum_path_pieces_0 = quantum_path_pieces_0 @@ -250,12 +246,16 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: else: source_0.is_entangled = True capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if([source_0] + quantum_path_pieces_0).equals([1] + [0] * len(quantum_path_pieces_0)).apply(alpha.Flip()).effect(capture_ancilla) + alpha.quantum_if([source_0] + quantum_path_pieces_0).equals( + [1] + [0] * len(quantum_path_pieces_0) + ).apply(alpha.Flip()).effect(capture_ancilla) could_capture = world.pop(capture_ancilla) if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? - print("Slide move not applied: either the source turns out be empty, or the path turns out to be blocked.") + print( + "Slide move not applied: either the source turns out be empty, or the path turns out to be blocked." + ) return # Apply the capture. # TODO(): we should implement and do unhook instead of force_measurement, @@ -264,7 +264,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: world.force_measurement(target_0, 0) target_0.reset() alpha.PhasedMove().effect(source_0, target_0) - # Pass the classical properties of source piece to the target piece. + # 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. @@ -273,13 +273,16 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: path_piece.reset() # For BASIC or EXCLUDED cases source_0.is_entangled = True - alpha.quantum_if(quantum_path_pieces_0).equals([0] * len(quantum_path_pieces_0)).apply(alpha.PhasedMove()).effect(source_0, target_0) + alpha.quantum_if(quantum_path_pieces_0).equals( + [0] * len(quantum_path_pieces_0) + ).apply(alpha.PhasedMove()).effect(source_0, target_0) # Copy the classical properties of the source piece to the target piece. target_0.reset(source_0) -class SplitSlide(Move): - def __init__(self, +class SplitSlide(QuantumEffect): + def __init__( + self, # source_0: Piece, # target_0: Piece, # target_1: Piece, @@ -300,12 +303,219 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0, target_1 = objects world = source_0.world - 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] - if len(quantum_path_pieces_0) ==0 and len(self.quantum_path_pieces_1) == 0: + 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) and + # TODO(): maybe move the above checks (if any path piece is one of the target pieces) # into classify_move(). SplitJump().effect(source_0, target_0, target_1) return + # TODO(): save ancillas for some specific scenarios. + path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + alpha.quantum_if(quantum_path_pieces_0).equals( + [0] * len(quantum_path_pieces_0) + ).apply(alpha.Flip()).effect(path_0_clear_ancilla) + path_1_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_1.name}") + alpha.quantum_if(quantum_path_pieces_1).equals( + [0] * len(quantum_path_pieces_1) + ).apply(alpha.Flip()).effect(path_1_clear_ancilla) + + # We do the normal split if both paths are clear. + alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + [1, 1] + ).apply(alpha.PhasedMove(0.5)).effect(source_0, target_0) + alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + [1, 1] + ).apply(alpha.PhasedMove()).effect(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()).effect(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()).effect(source_0, target_1) + + # TODO(): Do we need to zero-out, i.e. reverse those ancillas? + # Move the classical properties of the source piece to the target pieces. + target_0.reset(source_0) + target_1.reset(source_0) + source_0.reset() + + +class MergeSlide(QuantumEffect): + 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 split slide instead. + # TODO(): maybe move the above checks (if any path piece is one of the source pieces) + # into classify_move(). + MergeJump().effect(source_0, source_1, target_0) + return + # TODO(): save ancillas for some specific scenarios. + path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + alpha.quantum_if(quantum_path_pieces_0).equals( + [0] * len(quantum_path_pieces_0) + ).apply(alpha.Flip()).effect(path_0_clear_ancilla) + path_1_clear_ancilla = world._add_ancilla(f"{source_1.name}{target_0.name}") + alpha.quantum_if(quantum_path_pieces_1).equals( + [0] * len(quantum_path_pieces_1) + ).apply(alpha.Flip()).effect(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)).effect(source_0, target_0) + alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + [1, 1] + ).apply(alpha.PhasedMove(-0.5)).effect(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)).effect(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)).effect(source_0, target_1) + + # TODO(): Do we need to zero-out, i.e. reverse those ancillas? + # Move the classical properties of the source pieces to the target piece. + target_0.reset(source_0) + source_0.reset() + source_1.reset() + +class CannonFire(QuantumEffect): + 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] + if len(classical_path_pieces_0) == 1: + # In the case where there already is a cannon platform, the cannon could + # fire and capture only if quantum_path_pieces_0 are all empty. + could_capture = False + if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: + if not world.pop(quantum_path_pieces_0[0]): + quantum_path_pieces_0[0].reset() + could_capture = True + else: + source_0.is_entangled = True + capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") + alpha.quantum_if([source_0] + quantum_path_pieces_0).equals( + [1] + [0] * len(quantum_path_pieces_0) + ).apply(alpha.Flip()).effect(capture_ancilla) + could_capture = world.pop(capture_ancilla) + if not could_capture: + # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player + # could try to do this move again. Is this desired? + print( + "Cannon fire not applied: either the source turns out be empty, or the path turns out to be blocked." + ) + return + # Apply the capture. + # TODO(): we should implement and do unhook instead of force_measurement, + # since there are cases where target could be |1>. + if target_0.is_entangled: + world.force_measurement(target_0, 0) + target_0.reset() + alpha.PhasedMove().effect(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: + world.force_measurement(path_piece, 0) + path_piece.reset() + 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 occupied. + could_capture = False + source_0.is_entangled = True + # TODO(): think a more efficient way of implementing this case. + for index, expect_occupied_path_piece in enumerate(quantum_path_pieces_0): + capture_ancilla = world._add_ancilla( + f"{expect_occupied_path_piece.name}" + ) + expected_empty_pieces = [ + piece + for piece in quantum_path_pieces_0 + if piece.name != expect_occupied_path_piece.name + ] + alpha.quantum_if( + [source_0, expect_occupied_path_piece] + expected_empty_pieces + ).equals([1, 1] + [0] * len(expected_empty_pieces)).apply( + alpha.Flip() + ).effect( + capture_ancilla + ) + could_capture = world.pop(capture_ancilla) + if could_capture: + # Apply the capture. + # TODO(): we should implement and do unhook instead of force_measurement, + # since there are cases where target could be |1>. + if target_0.is_entangled: + world.force_measurement(target_0, 0) + target_0.reset() + alpha.PhasedMove().effect(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 expected_empty_pieces to be empty. + for empty_path_piece in expected_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, 0) + expect_occupied_path_piece.is_entangled = False + return + print( + "Cannon fire not applied: either the source turns out be empty, or there turns out to be (!=1) occupied path pieces." + ) + return From 026d1dffb88bc71f271f7187a588bc1b8cfb7b25 Mon Sep 17 00:00:00 2001 From: madcpf Date: Sun, 15 Oct 2023 00:05:30 -0700 Subject: [PATCH 04/31] update --- unitary/alpha/quantum_world.py | 9 ++ .../examples/quantum_chinese_chess/chess.py | 55 +++++++- .../examples/quantum_chinese_chess/enums.py | 50 +++++-- .../examples/quantum_chinese_chess/move.py | 131 +++++++++++------- 4 files changed, 175 insertions(+), 70 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index d5e6390d..7d008d54 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -510,3 +510,12 @@ def __getitem__(self, name: str) -> QuantumObject: if not quantum_object: raise KeyError(f"{name} did not exist in this world.") return quantum_object + + def unhook(self, object: QuantumObject) -> None: + new_ancilla = self._add_ancilla(object.name) + # Replace operations using the qubit of the given object with the ancilla instead + qubit_remapping_dict = {object.qubit: new_ancilla.qubit, new_ancilla.qubit: object.qubit} + self.circuit = self.circuit.transform_qubits( + lambda q: qubit_remapping_dict.get(q, q) + ) + return diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 3a296d6e..a3d9cdd1 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 Move, Jump +from unitary.examples.quantum_chinese_chess.move import ( + Jump, + Slide, + SplitJump, + SplitSlide, + MergeJump, + MergeSlide, + CannonFire, +) # List of accepable commands. _HELP_TEXT = """ @@ -356,10 +364,11 @@ def apply_move(self, str_to_parse: str) -> None: if target_0.type_ == Type.KING: # King is captured, then the game is over. self.game_state = GameState(self.current_player) - target_0.reset(source_0) - source_0.reset() + Jump(move_variant.CAPTURE)(source_0, target_0) + # target_0.reset(source_0) + # source_0.reset() # TODO(): only make such prints for a certain debug level. - print("Classical move.") + # print("Classical move.") elif move_type == MoveType.JUMP: Jump(move_variant)(source_0, target_0) elif move_type == MoveType.SLIDE: @@ -388,6 +397,21 @@ def next_move(self) -> bool: # The other player wins if the current player quits. self.game_state = GameState(1 - self.current_player) print("Exiting.") + elif input_str.lower() == "peek": + # TODO(): make it look like the normal board. Right now it's only for debugging purposes. + print(self.board.board.peek(convert_to_enum=False)) + elif input_str.lower() == "undo": + print("Undo last quantum effect.") + # Right now it's only for debugging purposes, since it has following problems: + # TODO(): there are several problems here: + # 1) last move is quantum but classical piece information is not reversed back. + # ==> we may need to save the change of classical piece information of each step. + # 2) last move might not be quantum. + # ==> we may need to save all classical moves and figure out how to undo each kind of move; + # 3) last move is quantum but involved multiple effects. + # ==> we may need to save number of effects per move, and undo that number of times. + self.board.board.undo_last_effect() + return True else: try: # The move is success if no ValueError is raised. @@ -398,6 +422,23 @@ def next_move(self) -> bool: print(e) return False + def update_board_by_sampling(self) -> None: + probs = self.board.board.get_binary_probabilities() + num_rows = 10 + num_cols = 9 + for row in range(num_rows): + for col in "abcdefghi": + piece = self.board.board[f"{col}{row}"] + # We need to do the following range() conversion since the sequence of + # qubits returned from get_binary_probabilities() is + # a9 b9 ... i9, a8 b8 ... i8, ..., a0 b0 ... i0 + prob = probs[(num_rows - row - 1) * num_cols + ord(col) - ord("a")] + # TODO(): set more accurate threshold + if prob < 1e-3: + piece.reset() + elif prob > 1 - 1e-3: + piece.is_entangled = False + def game_over(self) -> None: """Checks if the game is over, and update self.game_state accordingly.""" if self.game_state != GameState.CONTINUES: @@ -405,7 +446,6 @@ def game_over(self) -> None: if self.board.flying_general_check(): # If two KINGs are directly facing each other (i.e. in the same column) without any pieces in between, then the game ends. The other player wins. print("==== FLYING GENERAL ! ====") - print(self.board) self.game_state = GameState(1 - self.current_player) return # TODO(): add the following checks @@ -415,14 +455,17 @@ def play(self) -> None: """The loop where each player takes turn to play.""" while True: move_success = self.next_move() - print(self.board) if not move_success: # Continue if the player does not quit. if self.game_state == GameState.CONTINUES: + print(self.board) print("\nPlease re-enter your move.") continue # Check if the game is over. + # TODO(): maybe we should not check game_over() when an undo is made. + self.update_board_by_sampling() self.game_over() + print(self.board) if self.game_state == GameState.CONTINUES: # If the game continues, switch the player. self.current_player = 1 - self.current_player diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index 128f1843..485f2b91 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -16,17 +16,36 @@ class Language(enum.Enum): + """Currently we support the following two ways to print the board. + TODO(): support Chinese langurage instructions. + """ + EN = 0 # English ZH = 1 # Chinese class SquareState(enum.Enum): + """Defines two possible initial state of each piece of the board.""" + EMPTY = 0 OCCUPIED = 1 +class GameState(enum.Enum): + """Defines all possible outcomes of the game, plus the continue state.""" + + CONTINUES = -1 + RED_WINS = 0 + BLACK_WINS = 1 + DRAW = 2 + + +# TODO(): consider if we could allow split/merge + excluded/capture, +# and cannon_split_fire/cannon_merge_fire + capture class MoveType(enum.Enum): - NULL_TYPE = 0 + """Each valid move will be classfied into one of the following MoveTypes.""" + + CLASSICAL = 0 UNSPECIFIED_STANDARD = 1 JUMP = 2 SLIDE = 3 @@ -34,28 +53,30 @@ class MoveType(enum.Enum): SPLIT_SLIDE = 5 MERGE_JUMP = 6 MERGE_SLIDE = 7 - HORSE_MOVE = 8 - HORSE_SPLIT_MOVE = 9 - HORSE_MERGE_MOVE = 10 - CANNON_FIRE = 11 + CANNON_FIRE = 8 class MoveVariant(enum.Enum): - UNSPECIFIED = 0 - BASIC = 1 - EXCLUDED = 2 - CAPTURE = 3 + """Each valid move will be classfied into one of the following MoveVariant, in addition to + the MoveType above. + """ + + UNSPECIFIED = 0 # Used together with MoveType = CLASSICAL. + BASIC = 1 # The target piece is empty. + EXCLUDED = 2 # The target piece has the same color. + CAPTURE = 3 # The target piece has the opposite color. class Color(enum.Enum): - NA = 0 - RED = 1 - BLACK = 2 + """Empty pieces are associated with Color=NA. Other pieces should be either RED or BLACK.""" + + NA = -1 + RED = 0 + BLACK = 1 class Type(enum.Enum): - """ - The names are from FEN for Xiangqi. + """The names are from FEN for Xiangqi. The four values are symbols corresponding to - English red - English black @@ -87,6 +108,7 @@ def type_of(c: str) -> Optional["Type"]: @staticmethod def symbol(type_: "Type", color: Color, lang: Language = Language.EN) -> str: + """Returns symbol of the given piece according to its color and desired language.""" if type_ == Type.EMPTY: return "." if lang == Language.EN: # Return English symbols diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 9f829474..c9b02a75 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -127,11 +127,13 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0 = objects world = source_0.world if self.move_variant == MoveVariant.CAPTURE: - source_is_occupied = world.pop(source_0) + source_is_occupied = world.pop([source_0])[0] if not source_is_occupied: source_0.reset() print("Jump move: source turns out to be empty.") - return + return iter(()) + # TODO(): Note: pop and force_measurement could cause other pieces to actually + # turn back to classical piece or to empty, which is not checked/implemented currently. source_0.is_entangled = False # TODO(): we should implement and do unhook instead of force_measurement, # since there could be cases where target could be almost |1>. @@ -139,15 +141,16 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: world.force_measurement(target_0, 0) target_0.reset() elif self.move_variant == MoveVariant.EXCLUDED: - target_is_occupied = world.pop(target_0) + target_is_occupied = world.pop([target_0])[0] if target_is_occupied: print("Jump move: target turns out to be occupied.") target_0.is_entangled = False - return + return iter(()) target_0.reset() - alpha.PhasedMove().effect(source_0, target_0) + yield alpha.PhasedMove().effect(source_0, target_0) target_0.reset(source_0) source_0.reset() + return iter(()) class SplitJump(QuantumEffect): @@ -170,11 +173,12 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0, target_1 = objects source_0.is_entangled = True - alpha.PhasedSplit().effect(source_0, target_0, target_1) + yield alpha.PhasedSplit().effect(source_0, target_0, target_1) # Pass the classical properties of the source piece to the target pieces. target_0.reset(source_0) target_1.reset(source_0) source_0.reset() + return iter(()) class MergeJump(QuantumEffect): @@ -204,6 +208,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # TODO(): double check if we should do the following reset(). source_1.reset() source_0.reset() + return iter(()) class Slide(QuantumEffect): @@ -230,40 +235,41 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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) + target_is_occupied = world.pop([target_0])[0] if target_is_occupied: print("Slide move not applied: target turns out to be occupied.") target_0.is_entangled = False - return + 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: - if not world.pop(quantum_path_pieces_0[0]): + if not world.pop(quantum_path_pieces_0)[0]: quantum_path_pieces_0[0].reset() could_capture = True else: source_0.is_entangled = True capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if([source_0] + quantum_path_pieces_0).equals( + control_qubits = [source_0] + quantum_path_pieces_0 + yield alpha.quantum_if(*control_qubits).equals( [1] + [0] * len(quantum_path_pieces_0) ).apply(alpha.Flip()).effect(capture_ancilla) - could_capture = world.pop(capture_ancilla) + could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? print( "Slide move not applied: either the source turns out be empty, or the path turns out to be blocked." ) - return + return iter(()) # Apply the capture. # TODO(): we should implement and do unhook instead of force_measurement, # since there are cases where target could be |1>. if target_0.is_entangled: world.force_measurement(target_0, 0) target_0.reset() - alpha.PhasedMove().effect(source_0, target_0) + 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() @@ -273,11 +279,12 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: path_piece.reset() # For BASIC or EXCLUDED cases source_0.is_entangled = True - alpha.quantum_if(quantum_path_pieces_0).equals( + alpha.quantum_if(*quantum_path_pieces_0).equals( [0] * len(quantum_path_pieces_0) - ).apply(alpha.PhasedMove()).effect(source_0, target_0) + ).apply(alpha.PhasedMove())(source_0, target_0) # Copy the classical properties of the source piece to the target piece. target_0.reset(source_0) + return iter(()) class SplitSlide(QuantumEffect): @@ -315,32 +322,32 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # TODO(): maybe move the above checks (if any path piece is one of the target pieces) # into classify_move(). SplitJump().effect(source_0, target_0, target_1) - return + return iter(()) # TODO(): save ancillas for some specific scenarios. path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if(quantum_path_pieces_0).equals( + yield alpha.quantum_if(*quantum_path_pieces_0).equals( [0] * len(quantum_path_pieces_0) ).apply(alpha.Flip()).effect(path_0_clear_ancilla) path_1_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_1.name}") - alpha.quantum_if(quantum_path_pieces_1).equals( + yield alpha.quantum_if(*quantum_path_pieces_1).equals( [0] * len(quantum_path_pieces_1) ).apply(alpha.Flip()).effect(path_1_clear_ancilla) # We do the normal split if both paths are clear. - alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 1] ).apply(alpha.PhasedMove(0.5)).effect(source_0, target_0) - alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 1] ).apply(alpha.PhasedMove()).effect(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( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 0] ).apply(alpha.PhasedMove()).effect(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( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [0, 1] ).apply(alpha.PhasedMove()).effect(source_0, target_1) @@ -349,6 +356,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: target_0.reset(source_0) target_1.reset(source_0) source_0.reset() + return iter(()) class MergeSlide(QuantumEffect): @@ -381,32 +389,32 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # TODO(): maybe move the above checks (if any path piece is one of the source pieces) # into classify_move(). MergeJump().effect(source_0, source_1, target_0) - return + return iter(()) # TODO(): save ancillas for some specific scenarios. path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if(quantum_path_pieces_0).equals( + yield alpha.quantum_if(*quantum_path_pieces_0).equals( [0] * len(quantum_path_pieces_0) ).apply(alpha.Flip()).effect(path_0_clear_ancilla) path_1_clear_ancilla = world._add_ancilla(f"{source_1.name}{target_0.name}") - alpha.quantum_if(quantum_path_pieces_1).equals( + yield alpha.quantum_if(*quantum_path_pieces_1).equals( [0] * len(quantum_path_pieces_1) ).apply(alpha.Flip()).effect(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( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 1] ).apply(alpha.PhasedMove(-1.0)).effect(source_0, target_0) - alpha.quantum_if([path_0_clear_ancilla, path_1_clear_ancilla]).equals( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 1] ).apply(alpha.PhasedMove(-0.5)).effect(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( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [1, 0] ).apply(alpha.PhasedMove(-1.0)).effect(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( + yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( [0, 1] ).apply(alpha.PhasedMove(-1.0)).effect(source_0, target_1) @@ -415,6 +423,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: target_0.reset(source_0) source_0.reset() source_1.reset() + return iter(()) class CannonFire(QuantumEffect): @@ -436,35 +445,56 @@ 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 len(classical_path_pieces_0) == 1: + # Source has to be there to fire. + print(world.get_binary_probabilities()) + if not world.pop([source_0])[0]: + source_0.reset() + print("Cannonn fire not applied: source turns out to be empty.") + return iter(()) + # Target has to be there to fire. + if not world.pop([target_0])[0]: + target_0.reset() + print("Cannonn fire not applied: target turns out to be empty.") + return iter(()) + print(world.get_binary_probabilities()) + if len(self.classical_path_pieces_0) == 1: # In the case where there already is a cannon platform, the cannon could # fire and capture only if quantum_path_pieces_0 are all empty. could_capture = False if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: - if not world.pop(quantum_path_pieces_0[0]): + # Consider this special case to save an ancilla. + if not world.pop(quantum_path_pieces_0)[0]: quantum_path_pieces_0[0].reset() could_capture = True else: source_0.is_entangled = True capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - alpha.quantum_if([source_0] + quantum_path_pieces_0).equals( + # yield alpha.quantum_if(*control_qubits).equals( + # [1] + [0] * len(quantum_path_pieces_0) + # ).apply(alpha.Flip()).effect(capture_ancilla) + + # Note: test not use yield and effect + control_objects = [source_0] + quantum_path_pieces_0 + alpha.quantum_if(*control_objects).equals( [1] + [0] * len(quantum_path_pieces_0) - ).apply(alpha.Flip()).effect(capture_ancilla) - could_capture = world.pop(capture_ancilla) + ).apply(alpha.Flip())(capture_ancilla) + could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? print( - "Cannon fire not applied: either the source turns out be empty, or the path turns out to be blocked." + "Cannon fire not applied: the source turns out to be empty or the path turns out to be blocked." ) - return + return iter(()) # Apply the capture. + # MUST TODO # TODO(): we should implement and do unhook instead of force_measurement, # since there are cases where target could be |1>. - if target_0.is_entangled: - world.force_measurement(target_0, 0) + # if target_0.is_entangled: + # world.force_measurement(target_0, 0) + world.unhook(target_0) target_0.reset() - alpha.PhasedMove().effect(source_0, target_0) + 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() @@ -472,6 +502,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: for path_piece in quantum_path_pieces_0: 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 @@ -488,14 +519,14 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: for piece in quantum_path_pieces_0 if piece.name != expect_occupied_path_piece.name ] - alpha.quantum_if( - [source_0, expect_occupied_path_piece] + expected_empty_pieces - ).equals([1, 1] + [0] * len(expected_empty_pieces)).apply( - alpha.Flip() - ).effect( - capture_ancilla - ) - could_capture = world.pop(capture_ancilla) + control_qubits = [ + source_0, + expect_occupied_path_piece, + ] + expected_empty_pieces + yield alpha.quantum_if(*control_qubits).equals( + [1, 1] + [0] * len(expected_empty_pieces) + ).apply(alpha.Flip()).effect(capture_ancilla) + could_capture = world.pop([capture_ancilla])[0] if could_capture: # Apply the capture. # TODO(): we should implement and do unhook instead of force_measurement, @@ -503,7 +534,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: if target_0.is_entangled: world.force_measurement(target_0, 0) target_0.reset() - alpha.PhasedMove().effect(source_0, target_0) + yield alpha.PhasedMove().effect(source_0, target_0) # Move the classical properties of the source piece to the target piece. target_0.reset(source_0) source_0.reset() @@ -514,8 +545,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # Force measure the current expect_occupied_path_piece to be occupied. world.force_measurement(expect_occupied_path_piece, 0) expect_occupied_path_piece.is_entangled = False - return + return iter(()) print( "Cannon fire not applied: either the source turns out be empty, or there turns out to be (!=1) occupied path pieces." ) - return + return iter(()) From 8ef9e594fba6f0881225bec43210727e498863bd Mon Sep 17 00:00:00 2001 From: madcpf Date: Sun, 15 Oct 2023 00:06:09 -0700 Subject: [PATCH 05/31] up --- .../examples/quantum_chinese_chess/board.py | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 unitary/examples/quantum_chinese_chess/board.py diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py new file mode 100644 index 00000000..9f02b921 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -0,0 +1,223 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from typing import List, Tuple +import unitary.alpha as alpha +from unitary.examples.quantum_chinese_chess.enums import ( + SquareState, + Color, + Type, + Language, +) +from unitary.examples.quantum_chinese_chess.piece import Piece + + +# The default initial state of the game. +_INITIAL_FEN = "RHEAKAEHR/9/1C5C1/P1P1P1P1P/9/9/p1p1p1p1p/1c5c1/9/rheakaehr w---1" + + +class Board: + """Board holds the assemble of all pieces. Each piece could be either in classical or quantum state.""" + + def __init__( + self, board: alpha.QuantumWorld, current_player: int, king_locations: List[str] + ): + self.board = board + self.current_player = current_player + # This saves the locations of KINGs in the order of [RED_KING_LOCATION, BLACK_KING_LOCATION]. + self.king_locations = king_locations + self.lang = Language.EN # The default language is English. + + def set_language(self, lang: Language): + self.lang = lang + + @classmethod + def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": + """ + Translates FEN (Forsyth-Edwards Notation) symbols into the whole QuantumWorld board. + FEN rule for Chinese Chess could be found at https://www.wxf-xiangqi.org/images/computer-xiangqi/fen-for-xiangqi-chinese-chess.pdf + """ + chess_board = {} + row_index = 9 + king_locations = [] + pieces, turns = fen.split(" ", 1) + for row in pieces.split("/"): + col = ord("a") + for char in row: + # Add empty board pieces. + if "1" <= char <= "9": + for i in range(int(char)): + name = f"{chr(col)}{row_index}" + chess_board[name] = Piece( + name, SquareState.EMPTY, Type.EMPTY, Color.NA + ) + col += 1 + # Add occupied board pieces. + else: + name = f"{chr(col)}{row_index}" + piece_type = Type.type_of(char) + if piece_type == Type.KING: + king_locations.append(name) + color = Color.RED if char.isupper() else Color.BLACK + chess_board[name] = Piece( + name, SquareState.OCCUPIED, piece_type, color + ) + col += 1 + row_index -= 1 + board = alpha.QuantumWorld(chess_board.values()) + # Here 0 means the player RED while 1 the player BLACK. + current_player = 0 if "w" in turns else 1 + # TODO(): maybe add check to make sure the input fen itself is correct. + if len(king_locations) != 2: + raise ValueError( + f"We expect two KINGs on the board, but got {len(king_locations)}." + ) + return cls(board, current_player, king_locations) + + def __str__(self): + num_rows = 10 + board_string = ["\n "] + probs = self.board.get_binary_probabilities() + # Print the top line of col letters. + for col in "abcdefghi": + board_string.append(f" {col} ") + board_string.append("\n") + for row in range(num_rows): + # Print the row index on the left. + board_string.append(f"{row} ") + for col in "abcdefghi": + piece = self.board[f"{col}{row}"] + board_string += piece.symbol(self.lang) + if self.lang == Language.EN: + board_string.append(" ") + # Print the row index on the right. + board_string.append(f" {row}\n ") + # Print the sampled prob. of the pieces in the above row. + # We need to do the following range() conversion since the sequence of + # qubits returned from get_binary_probabilities() is + # a9 b9 ... i9, a8 b8 ... i8, ..., a0 b0 ... i0 + for i in range((num_rows - row - 1) * 9, (num_rows - row) * 9): + board_string.append("{:.1f} ".format(probs[i])) + board_string.append("\n") + board_string.append(" ") + # Print the bottom line of col letters. + for col in "abcdefghi": + board_string.append(f" {col} ") + board_string.append("\n") + if self.lang == Language.EN: + return "".join(board_string) + # We need to turn letters into their full-width counterparts to align + # a mix of letters + Chinese characters. + chars = "".join(chr(c) for c in range(ord(" "), ord("z"))) + full_width_chars = "\N{IDEOGRAPHIC SPACE}" + "".join( + chr(c) + for c in range( + ord("\N{FULLWIDTH EXCLAMATION MARK}"), + ord("\N{FULLWIDTH LATIN SMALL LETTER Z}"), + ) + ) + translation = str.maketrans(chars, full_width_chars) + return ( + "".join(board_string) + .replace(" ", "") + .replace("abcdefghi", " abcdefghi") + .translate(translation) + ) + + def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: + """Returns the nonempty classical and quantum pieces from source to target (excluded).""" + x0 = ord(source[0]) + x1 = ord(target[0]) + dx = x1 - x0 + y0 = int(source[1]) + y1 = int(target[1]) + dy = y1 - y0 + # In case of only moving one step, return empty path pieces. + if abs(dx) + abs(dy) <= 1: + return [], [] + # In case of advisor moving, return empty path pieces. + # TODO(): maybe move this to the advisor move check. + if abs(dx) == 1 and abs(dy) == 1: + return [], [] + pieces = [] + classical_pieces = [] + quantum_pieces = [] + dx_sign = np.sign(dx) + dy_sign = np.sign(dy) + # In case of elephant move, there should only be one path piece. + if abs(dx) == abs(dy): + pieces.append(f"{chr(x0 + dx_sign)}{y0 + dy_sign}") + # This could be move of rook, king, pawn or cannon. + elif dx == 0: + for i in range(1, abs(dy)): + pieces.append(f"{chr(x0)}{y0 + dy_sign * i}") + # This could be move of rook, king, pawn or cannon. + elif dy == 0: + for i in range(1, abs(dx)): + pieces.append(f"{chr(x0 + dx_sign * i)}{y0}") + # This covers four possible directions of horse move. + elif abs(dx) == 2 and abs(dy) == 1: + pieces.append(f"{chr(x0 + dx_sign)}{y0}") + # This covers the other four possible directions of horse move. + elif abs(dy) == 2 and abs(dx) == 1: + pieces.append(f"{chr(x0)}{y0 + dy_sign}") + else: + raise ValueError("Unexpected input to path_pieces().") + for piece in pieces: + if self.board[piece].is_entangled: + quantum_pieces.append(piece) + elif self.board[piece].type_ != Type.EMPTY: + classical_pieces.append(piece) + return classical_pieces, quantum_pieces + + def flying_general_check(self) -> bool: + """Check and return if the two KINGs are directly facing each other (i.e. in the same column) without any pieces in between.""" + king_0 = self.king_locations[0] + king_1 = self.king_locations[1] + if king_0[0] != king_1[0]: + # If they are in different columns, the check fails. Game continues. + return False + classical_pieces, quantum_pieces = self.path_pieces(king_0, king_1) + if len(classical_pieces) > 0: + # If there are classical pieces between two KINGs, the check fails. Game continues. + return False + if len(quantum_pieces) == 0: + # If there are no pieces between two KINGs, the check successes. Game ends. + return True + # When there are quantum pieces in between, the check successes (and game ends) + # if there are at lease one occupied path piece. + capture_ancilla = self.board._add_ancilla("flying_general_check") + alpha.quantum_if(quantum_pieces).equals([0] * len(quantum_pieces)).apply( + alpha.Flip() + ).effect(capture_ancilla) + could_capture = self.board.pop(capture_ancilla) + if could_capture: + # Force measure all path pieces to be empty. + for path_piece in quantum_pieces: + self.board.force_measurement(path_piece, 0) + path_piece.reset() + + # Let the general/king fly. + current_king = self.board[self.king_locations[self.current_player]] + oppsite_king = self.board[self.king_locations[1 - self.current_player]] + current_king.reset(oppsite_king) + oppsite_king.reset() + print("==== FLYING GENERAL ! ====") + print(self) + return True + else: + # TODO(): we are leaving the path pieces unchanged in entangled state. Maybe + # better to force measure them? One option is to randomly chosing one path piece + # and force measure it to be occupied. + return False From b5a5a3d62779a06491823bee227c1383c601d4fd Mon Sep 17 00:00:00 2001 From: madcpf Date: Tue, 17 Oct 2023 11:36:04 -0700 Subject: [PATCH 06/31] update --- .../examples/quantum_chinese_chess/board.py | 43 +++- .../examples/quantum_chinese_chess/chess.py | 47 ++-- .../examples/quantum_chinese_chess/enums.py | 13 +- .../examples/quantum_chinese_chess/move.py | 207 ++++++++---------- 4 files changed, 157 insertions(+), 153 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index 9f02b921..2bfa84c3 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -22,6 +22,16 @@ ) from unitary.examples.quantum_chinese_chess.piece import Piece +reset = "\033[0m" +bold = "\033[01m" +dim = "\033[02m" + +# background +grey = "\033[47m" + +# foreground +black = "\033[30m" +red = "\033[31m" # The default initial state of the game. _INITIAL_FEN = "RHEAKAEHR/9/1C5C1/P1P1P1P1P/9/9/p1p1p1p1p/1c5c1/9/rheakaehr w---1" @@ -85,10 +95,12 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": ) return cls(board, current_player, king_locations) - def __str__(self): + def to_str(self, probs: List[float] = None): + # TODO(): print players' names in their corresponding side of the board. num_rows = 10 board_string = ["\n "] - probs = self.board.get_binary_probabilities() + if probs is None: + probs = self.board.get_binary_probabilities() # Print the top line of col letters. for col in "abcdefghi": board_string.append(f" {col} ") @@ -98,9 +110,16 @@ def __str__(self): board_string.append(f"{row} ") for col in "abcdefghi": piece = self.board[f"{col}{row}"] - board_string += piece.symbol(self.lang) - if self.lang == Language.EN: + if piece.is_entangled: + board_string += dim + if piece.color == Color.RED: + board_string += red + else: + board_string += black + board_string += grey + bold + piece.symbol(self.lang) + if self.lang == Language.EN and col != "i": board_string.append(" ") + board_string += reset # Print the row index on the right. board_string.append(f" {row}\n ") # Print the sampled prob. of the pieces in the above row. @@ -173,7 +192,7 @@ def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: elif abs(dy) == 2 and abs(dx) == 1: pieces.append(f"{chr(x0)}{y0 + dy_sign}") else: - raise ValueError("Unexpected input to path_pieces().") + raise ValueError("The input move is illegal.") for piece in pieces: if self.board[piece].is_entangled: quantum_pieces.append(piece) @@ -198,13 +217,15 @@ def flying_general_check(self) -> bool: # When there are quantum pieces in between, the check successes (and game ends) # if there are at lease one occupied path piece. capture_ancilla = self.board._add_ancilla("flying_general_check") - alpha.quantum_if(quantum_pieces).equals([0] * len(quantum_pieces)).apply( - alpha.Flip() - ).effect(capture_ancilla) - could_capture = self.board.pop(capture_ancilla) + control_objects = [self.board[path] for path in quantum_pieces] + conditions = [0] * len(control_objects) + alpha.quantum_if(*control_objects).equals(*conditions).apply(alpha.Flip())( + capture_ancilla + ) + could_capture = self.board.pop([capture_ancilla])[0] if could_capture: # Force measure all path pieces to be empty. - for path_piece in quantum_pieces: + for path_piece in control_objects: self.board.force_measurement(path_piece, 0) path_piece.reset() @@ -214,10 +235,10 @@ def flying_general_check(self) -> bool: current_king.reset(oppsite_king) oppsite_king.reset() print("==== FLYING GENERAL ! ====") - print(self) return True else: # TODO(): we are leaving the path pieces unchanged in entangled state. Maybe # better to force measure them? One option is to randomly chosing one path piece # and force measure it to be occupied. + print("==== General not flies yet ! ====") return False diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index a3d9cdd1..90c43c12 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -30,6 +30,7 @@ MergeSlide, CannonFire, ) +import readline # List of accepable commands. _HELP_TEXT = """ @@ -72,7 +73,7 @@ def __init__(self): self.print_welcome() self.board = Board.from_fen() self.board.set_language(self.lang) - print(self.board) + print(self.board.to_str()) self.game_state = GameState.CONTINUES self.current_player = self.board.current_player self.debug_level = 3 @@ -226,7 +227,7 @@ def classify_move( quantum_path_pieces_1: List[str], ) -> Tuple[MoveType, MoveVariant]: """Determines the MoveType and MoveVariant.""" - move_type = MoveType.UNSPECIFIED_STANDARD + move_type = MoveType.UNSPECIFIED move_variant = MoveVariant.UNSPECIFIED source = self.board.board[sources[0]] @@ -243,18 +244,18 @@ def classify_move( "CANNON could not fire/capture without a cannon platform." ) if not source.is_entangled and not target.is_entangled: - return MoveType.CLASSICAL, MoveVariant.UNSPECIFIED + return MoveType.CLASSICAL, MoveVariant.CLASSICAL else: move_type = MoveType.JUMP else: move_type = MoveType.SLIDE if ( - move_type != MoveType.CLASSICAL - and source.type_ == Type.CANNON + source.type_ == Type.CANNON and ( len(classical_path_pieces_0) == 1 or len(quantum_path_pieces_0) > 0 ) + and target.color.value == 1 - source.color.value ): # By this time the classical cannon fire has been identified as CLASSICAL JUMP. return MoveType.CANNON_FIRE, MoveVariant.CAPTURE @@ -355,6 +356,8 @@ def apply_move(self, str_to_parse: str) -> None: quantum_pieces_1, ) + print(move_type, " ", move_variant) + if move_type == MoveType.CLASSICAL: if source_0.type_ == Type.KING: # Update the locations of KING. @@ -364,7 +367,7 @@ def apply_move(self, str_to_parse: str) -> None: if target_0.type_ == Type.KING: # King is captured, then the game is over. self.game_state = GameState(self.current_player) - Jump(move_variant.CAPTURE)(source_0, target_0) + Jump(move_variant)(source_0, target_0) # target_0.reset(source_0) # source_0.reset() # TODO(): only make such prints for a certain debug level. @@ -384,24 +387,25 @@ def apply_move(self, str_to_parse: str) -> None: elif move_type == MoveType.CANNON_FIRE: CannonFire(classical_pieces_0, quantum_pieces_0)(source_0, target_0) - def next_move(self) -> bool: + def next_move(self) -> Tuple[bool, str]: """Check if the player wants to exit or needs help message. Otherwise parse and apply the move. - Returns True if the move was made, otherwise returns False. + Returns True + output string if the move was made, otherwise returns False + output string. """ input_str = input( f"\nIt is {self.players_name[self.current_player]}'s turn to move: " ) + output = "" if input_str.lower() == "help": - print(_HELP_TEXT) + output = _HELP_TEXT elif input_str.lower() == "exit": # The other player wins if the current player quits. self.game_state = GameState(1 - self.current_player) - print("Exiting.") + output = "Exiting." elif input_str.lower() == "peek": # TODO(): make it look like the normal board. Right now it's only for debugging purposes. print(self.board.board.peek(convert_to_enum=False)) elif input_str.lower() == "undo": - print("Undo last quantum effect.") + output = "Undo last quantum effect." # Right now it's only for debugging purposes, since it has following problems: # TODO(): there are several problems here: # 1) last move is quantum but classical piece information is not reversed back. @@ -411,18 +415,17 @@ def next_move(self) -> bool: # 3) last move is quantum but involved multiple effects. # ==> we may need to save number of effects per move, and undo that number of times. self.board.board.undo_last_effect() - return True + return True, output else: try: # The move is success if no ValueError is raised. self.apply_move(input_str.lower()) - return True + return True, output except ValueError as e: - print("Invalid move.") - print(e) - return False + output = f"Invalid move. {e}" + return False, output - def update_board_by_sampling(self) -> None: + def update_board_by_sampling(self) -> List[float]: probs = self.board.board.get_binary_probabilities() num_rows = 10 num_cols = 9 @@ -445,7 +448,6 @@ def game_over(self) -> None: return if self.board.flying_general_check(): # If two KINGs are directly facing each other (i.e. in the same column) without any pieces in between, then the game ends. The other player wins. - print("==== FLYING GENERAL ! ====") self.game_state = GameState(1 - self.current_player) return # TODO(): add the following checks @@ -454,18 +456,19 @@ def game_over(self) -> None: def play(self) -> None: """The loop where each player takes turn to play.""" while True: - move_success = self.next_move() + move_success, output = self.next_move() if not move_success: # Continue if the player does not quit. if self.game_state == GameState.CONTINUES: - print(self.board) + print(output) print("\nPlease re-enter your move.") continue + print(output) # Check if the game is over. # TODO(): maybe we should not check game_over() when an undo is made. - self.update_board_by_sampling() self.game_over() - print(self.board) + probs = self.update_board_by_sampling() + print(self.board.to_str(probs)) if self.game_state == GameState.CONTINUES: # If the game continues, switch the player. self.current_player = 1 - self.current_player diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index 485f2b91..a4c46120 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -45,8 +45,8 @@ class GameState(enum.Enum): class MoveType(enum.Enum): """Each valid move will be classfied into one of the following MoveTypes.""" - CLASSICAL = 0 - UNSPECIFIED_STANDARD = 1 + UNSPECIFIED = 0 + CLASSICAL = 1 JUMP = 2 SLIDE = 3 SPLIT_JUMP = 4 @@ -61,10 +61,11 @@ class MoveVariant(enum.Enum): the MoveType above. """ - UNSPECIFIED = 0 # Used together with MoveType = CLASSICAL. - BASIC = 1 # The target piece is empty. - EXCLUDED = 2 # The target piece has the same color. - CAPTURE = 3 # The target piece has the opposite color. + UNSPECIFIED = 0 + CLASSICAL = 1 # Used together with MoveType = CLASSICAL. + BASIC = 2 # The target piece is empty. + EXCLUDED = 3 # The target piece has the same color. + CAPTURE = 4 # The target piece has the opposite color. class Color(enum.Enum): diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index c9b02a75..f94372ae 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -20,6 +20,8 @@ from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type +# TODO(): now the class is no longer the base class of all chess moves. Maybe convert this class +# to a helper class to save each move (with its pop results) in a string form into move history. class Move(QuantumEffect): """The base class of all chess moves.""" @@ -105,12 +107,8 @@ def __str__(self): class Jump(QuantumEffect): def __init__( self, - # source_0: Piece, - # target_0: Piece, - # board: Board, move_variant: MoveVariant, ): - # super().__init__(source_0, target_0, board, move_type=MoveType.JUMP, move_variant=move_variant) self.move_variant = move_variant def num_dimension(self) -> Optional[int]: @@ -121,7 +119,7 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: # TODO(): currently pawn capture is a same as jump capture, while in quantum chess it's different, - # i.e. pawn would only move if the source is there, i.e. CNOT(t, s), and an entanglement could be + # i.e. pawn would move only if the target is there, i.e. CNOT(t, s), and an entanglement could be # created. This could be a general game setting, i.e. we could allow players to choose if they # want the source piece to move (in case of capture) if the target piece is not there. source_0, target_0 = objects @@ -132,13 +130,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0.reset() print("Jump move: source turns out to be empty.") return iter(()) - # TODO(): Note: pop and force_measurement could cause other pieces to actually - # turn back to classical piece or to empty, which is not checked/implemented currently. source_0.is_entangled = False - # TODO(): we should implement and do unhook instead of force_measurement, - # since there could be cases where target could be almost |1>. - if target_0.is_entangled: - world.force_measurement(target_0, 0) + world.unhook(target_0) target_0.reset() elif self.move_variant == MoveVariant.EXCLUDED: target_is_occupied = world.pop([target_0])[0] @@ -147,7 +140,12 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: target_0.is_entangled = False return iter(()) target_0.reset() - yield alpha.PhasedMove().effect(source_0, target_0) + elif self.move_variant == MoveVariant.CLASSICAL: + 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() return iter(()) @@ -156,13 +154,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: class SplitJump(QuantumEffect): def __init__( self, - # source_0: Piece, - # target_0: Piece, - # target_1: Piece, - # board: Board, ): - # super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, target_1 = target_1) - pass + return def num_dimension(self) -> Optional[int]: return 2 @@ -172,8 +165,9 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0, target_1 = objects + # Make the split jump. source_0.is_entangled = True - yield alpha.PhasedSplit().effect(source_0, target_0, target_1) + alpha.PhasedSplit()(source_0, target_0, target_1) # Pass the classical properties of the source piece to the target pieces. target_0.reset(source_0) target_1.reset(source_0) @@ -184,13 +178,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: class MergeJump(QuantumEffect): def __init__( self, - # source_0: Piece, - # source_1: Piece, - # target_0: Piece, - # board: Board, ): - # super().__init__(source_0, target_0, board, move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.BASIC, source_1) - pass + return def num_dimension(self) -> Optional[int]: return 2 @@ -200,27 +189,21 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, source_1, target_0 = objects - yield cirq.ISWAP(source_0, target_0) ** -0.5 - yield cirq.ISWAP(source_0, target_0) ** -0.5 - yield cirq.ISWAP(source_1, target_0) ** -0.5 + # Make the merge jump. + cirq.ISWAP(source_0.qubit, target_0.qubit) ** -0.5 + cirq.ISWAP(source_0.qubit, target_0.qubit) ** -0.5 + cirq.ISWAP(source_1.qubit, target_0.qubit) ** -0.5 # Pass the classical properties of the source pieces to the target piece. target_0.reset(source_0) - # TODO(): double check if we should do the following reset(). - source_1.reset() - source_0.reset() return iter(()) class Slide(QuantumEffect): def __init__( self, - # source_0: Piece, - # target_0: Piece, quantum_path_pieces_0: List[str], - # board: Board, move_variant: MoveVariant, ): - # super().__init__(source_0, target_0, board, move_type=MoveType.SLIDE, move_variant=move_variant) self.quantum_path_pieces_0 = quantum_path_pieces_0 self.move_variant = move_variant @@ -252,9 +235,10 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 - yield alpha.quantum_if(*control_qubits).equals( - [1] + [0] * len(quantum_path_pieces_0) - ).apply(alpha.Flip()).effect(capture_ancilla) + conditions = [1] + [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*control_qubits).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player @@ -264,10 +248,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ) return iter(()) # Apply the capture. - # TODO(): we should implement and do unhook instead of force_measurement, - # since there are cases where target could be |1>. - if target_0.is_entangled: - world.force_measurement(target_0, 0) + 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. @@ -279,25 +260,23 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: path_piece.reset() # For BASIC or EXCLUDED cases source_0.is_entangled = True - alpha.quantum_if(*quantum_path_pieces_0).equals( - [0] * len(quantum_path_pieces_0) - ).apply(alpha.PhasedMove())(source_0, target_0) + conditions = [0] * len(quantum_path_pieces_0) + 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) here since there is a non-zero probability + # that the source is not moved. return iter(()) class SplitSlide(QuantumEffect): def __init__( self, - # source_0: Piece, - # target_0: Piece, - # target_1: Piece, quantum_path_pieces_0: List[str], quantum_path_pieces_1: List[str], - # board: Board, ): - # super().__init__(source_0, target_0, board, move_type=MoveType.SPLIT_SLIDE, move_variant=MoveVariant.BASIC, target_1=target_1) self.quantum_path_pieces_0 = quantum_path_pieces_0 self.quantum_path_pieces_1 = quantum_path_pieces_1 @@ -321,41 +300,51 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # 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(). - SplitJump().effect(source_0, target_0, target_1) + SplitJump()(source_0, target_0, target_1) return iter(()) # TODO(): save ancillas for some specific scenarios. path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - yield alpha.quantum_if(*quantum_path_pieces_0).equals( - [0] * len(quantum_path_pieces_0) - ).apply(alpha.Flip()).effect(path_0_clear_ancilla) + if len(quantum_path_pieces_0) == 0: + alpha.Flip()(path_0_clear_ancilla) + else: + conditions = [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*quantum_path_pieces_0).equals(*conditions).apply( + alpha.Flip() + )(path_0_clear_ancilla) path_1_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_1.name}") - yield alpha.quantum_if(*quantum_path_pieces_1).equals( - [0] * len(quantum_path_pieces_1) - ).apply(alpha.Flip()).effect(path_1_clear_ancilla) + + if len(quantum_path_pieces_1) == 0: + alpha.Flip()(path_1_clear_ancilla) + else: + 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 1] - ).apply(alpha.PhasedMove(0.5)).effect(source_0, target_0) - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 1] - ).apply(alpha.PhasedMove()).effect(source_0, target_1) + 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 0] - ).apply(alpha.PhasedMove()).effect(source_0, 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [0, 1] - ).apply(alpha.PhasedMove()).effect(source_0, target_1) + alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals(0, 1).apply( + alpha.PhasedMove() + )(source_0, target_1) # TODO(): Do we need to zero-out, i.e. reverse those ancillas? # Move the classical properties of the source piece to the target pieces. target_0.reset(source_0) target_1.reset(source_0) - source_0.reset() + # 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. return iter(()) @@ -388,41 +377,44 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # If both paths are empty, do split slide instead. # TODO(): maybe move the above checks (if any path piece is one of the source pieces) # into classify_move(). - MergeJump().effect(source_0, source_1, target_0) + MergeJump()(source_0, source_1, target_0) return iter(()) # TODO(): save ancillas for some specific scenarios. path_0_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - yield alpha.quantum_if(*quantum_path_pieces_0).equals( - [0] * len(quantum_path_pieces_0) - ).apply(alpha.Flip()).effect(path_0_clear_ancilla) + path_0_conditions = [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*quantum_path_pieces_0).equals(*path_0_conditions).apply( + alpha.Flip() + )(path_0_clear_ancilla) + path_1_clear_ancilla = world._add_ancilla(f"{source_1.name}{target_0.name}") - yield alpha.quantum_if(*quantum_path_pieces_1).equals( - [0] * len(quantum_path_pieces_1) - ).apply(alpha.Flip()).effect(path_1_clear_ancilla) + path_1_conditions = [0] * len(quantum_path_pieces_1) + 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 1] - ).apply(alpha.PhasedMove(-1.0)).effect(source_0, target_0) - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 1] - ).apply(alpha.PhasedMove(-0.5)).effect(source_1, target_0) + 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [1, 0] - ).apply(alpha.PhasedMove(-1.0)).effect(source_0, 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. - yield alpha.quantum_if(path_0_clear_ancilla, path_1_clear_ancilla).equals( - [0, 1] - ).apply(alpha.PhasedMove(-1.0)).effect(source_0, target_1) + 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? # Move the classical properties of the source pieces to the target piece. target_0.reset(source_0) - source_0.reset() - source_1.reset() + # 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. return iter(()) @@ -446,7 +438,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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. - print(world.get_binary_probabilities()) if not world.pop([source_0])[0]: source_0.reset() print("Cannonn fire not applied: source turns out to be empty.") @@ -456,7 +447,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: target_0.reset() print("Cannonn fire not applied: target turns out to be empty.") return iter(()) - print(world.get_binary_probabilities()) if len(self.classical_path_pieces_0) == 1: # In the case where there already is a cannon platform, the cannon could # fire and capture only if quantum_path_pieces_0 are all empty. @@ -469,15 +459,11 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: else: source_0.is_entangled = True capture_ancilla = world._add_ancilla(f"{source_0.name}{target_0.name}") - # yield alpha.quantum_if(*control_qubits).equals( - # [1] + [0] * len(quantum_path_pieces_0) - # ).apply(alpha.Flip()).effect(capture_ancilla) - - # Note: test not use yield and effect control_objects = [source_0] + quantum_path_pieces_0 - alpha.quantum_if(*control_objects).equals( - [1] + [0] * len(quantum_path_pieces_0) - ).apply(alpha.Flip())(capture_ancilla) + conditions = [1] + [0] * len(quantum_path_pieces_0) + alpha.quantum_if(*control_objects).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player @@ -487,11 +473,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ) return iter(()) # Apply the capture. - # MUST TODO - # TODO(): we should implement and do unhook instead of force_measurement, - # since there are cases where target could be |1>. - # if target_0.is_entangled: - # world.force_measurement(target_0, 0) world.unhook(target_0) target_0.reset() alpha.PhasedMove()(source_0, target_0) @@ -523,18 +504,16 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, expect_occupied_path_piece, ] + expected_empty_pieces - yield alpha.quantum_if(*control_qubits).equals( - [1, 1] + [0] * len(expected_empty_pieces) - ).apply(alpha.Flip()).effect(capture_ancilla) + conditions = [1, 1] + [0] * len(expected_empty_pieces) + alpha.quantum_if(*control_qubits).equals(*conditions).apply( + alpha.Flip() + )(capture_ancilla) could_capture = world.pop([capture_ancilla])[0] if could_capture: # Apply the capture. - # TODO(): we should implement and do unhook instead of force_measurement, - # since there are cases where target could be |1>. - if target_0.is_entangled: - world.force_measurement(target_0, 0) + world.unhook(target_0) target_0.reset() - yield alpha.PhasedMove().effect(source_0, target_0) + 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() From 548583a0fb7ef16afebae5ce1b04496ed342d028 Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Tue, 17 Oct 2023 14:50:33 -0700 Subject: [PATCH 07/31] update --- .../examples/quantum_chinese_chess/board.py | 64 +++++++++++++------ .../examples/quantum_chinese_chess/enums.py | 4 +- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index 2bfa84c3..ff80ac08 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -25,6 +25,10 @@ reset = "\033[0m" bold = "\033[01m" dim = "\033[02m" +negative = "\033[03m" +underline = "\033[04m" +blink = "\033[05m" +invisible = "\033[08m" # background grey = "\033[47m" @@ -103,22 +107,32 @@ def to_str(self, probs: List[float] = None): probs = self.board.get_binary_probabilities() # Print the top line of col letters. for col in "abcdefghi": - board_string.append(f" {col} ") + if self.lang == Language.EN: + board_string.append(f" {col} ") + else: + board_string.append(f"{col} ") board_string.append("\n") for row in range(num_rows): # Print the row index on the left. - board_string.append(f"{row} ") + if self.lang == Language.EN: + board_string.append(f"{row} ") + else: + board_string.append(f"{row} ") for col in "abcdefghi": piece = self.board[f"{col}{row}"] + board_string += grey if piece.is_entangled: board_string += dim if piece.color == Color.RED: board_string += red else: board_string += black - board_string += grey + bold + piece.symbol(self.lang) - if self.lang == Language.EN and col != "i": - board_string.append(" ") + board_string += piece.symbol(self.lang) + if col != "i": + if self.lang == Language.EN: + board_string.append(" ") + else: + board_string.append(" ") board_string += reset # Print the row index on the right. board_string.append(f" {row}\n ") @@ -132,27 +146,39 @@ def to_str(self, probs: List[float] = None): board_string.append(" ") # Print the bottom line of col letters. for col in "abcdefghi": - board_string.append(f" {col} ") + if self.lang == Language.EN: + board_string.append(f" {col} ") + else: + board_string.append(f"{col} ") board_string.append("\n") if self.lang == Language.EN: return "".join(board_string) # We need to turn letters into their full-width counterparts to align # a mix of letters + Chinese characters. - chars = "".join(chr(c) for c in range(ord(" "), ord("z"))) - full_width_chars = "\N{IDEOGRAPHIC SPACE}" + "".join( - chr(c) - for c in range( - ord("\N{FULLWIDTH EXCLAMATION MARK}"), - ord("\N{FULLWIDTH LATIN SMALL LETTER Z}"), - ) + half_width_chars = "".join( + [" ", "\uFF65"] + + [chr(c) for c in range(ord("A"), ord("Z"))] + + [chr(c) for c in range(ord("a"), ord("z"))] ) - translation = str.maketrans(chars, full_width_chars) - return ( - "".join(board_string) - .replace(" ", "") - .replace("abcdefghi", " abcdefghi") - .translate(translation) + full_width_chars = "".join( + ["\N{IDEOGRAPHIC SPACE}", "\u30FB"] + + [ + chr(c) + for c in range( + ord("\N{FULLWIDTH LATIN CAPITAL LETTER A}"), + ord("\N{FULLWIDTH LATIN CAPITAL LETTER Z}"), + ) + ] + + [ + chr(c) + for c in range( + ord("\N{FULLWIDTH LATIN SMALL LETTER A}"), + ord("\N{FULLWIDTH LATIN SMALL LETTER Z}"), + ) + ] ) + translation = str.maketrans(half_width_chars, full_width_chars) + return "".join(board_string).translate(translation) def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: """Returns the nonempty classical and quantum pieces from source to target (excluded).""" diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index a4c46120..f7f44112 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -85,7 +85,7 @@ class Type(enum.Enum): - Chinese black """ - EMPTY = (".", ".", ".", ".") + EMPTY = ("\uFF65", "\uFF65", "\uFF65", "\uFF65") PAWN = ("P", "p", "兵", "卒") CANNON = ("C", "c", "炮", "砲") ROOK = ("R", "r", "车", "車") @@ -111,7 +111,7 @@ def type_of(c: str) -> Optional["Type"]: def symbol(type_: "Type", color: Color, lang: Language = Language.EN) -> str: """Returns symbol of the given piece according to its color and desired language.""" if type_ == Type.EMPTY: - return "." + return type_.value[0] if lang == Language.EN: # Return English symbols if color == Color.RED: return type_.value[0] From dec934e94dbad2ab7839ff74239db1baaf59fbbb Mon Sep 17 00:00:00 2001 From: madcpf Date: Tue, 17 Oct 2023 23:57:01 -0700 Subject: [PATCH 08/31] update --- .../examples/quantum_chinese_chess/board.py | 2 +- .../examples/quantum_chinese_chess/move.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index ff80ac08..ca33f3a2 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -248,7 +248,7 @@ def flying_general_check(self) -> bool: alpha.quantum_if(*control_objects).equals(*conditions).apply(alpha.Flip())( capture_ancilla ) - could_capture = self.board.pop([capture_ancilla])[0] + could_capture = self.board.pop([capture_ancilla])[0].value if could_capture: # Force measure all path pieces to be empty. for path_piece in control_objects: diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index f94372ae..0a347073 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -125,7 +125,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, target_0 = objects world = source_0.world if self.move_variant == MoveVariant.CAPTURE: - source_is_occupied = world.pop([source_0])[0] + source_is_occupied = world.pop([source_0])[0].value if not source_is_occupied: source_0.reset() print("Jump move: source turns out to be empty.") @@ -134,7 +134,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: world.unhook(target_0) target_0.reset() elif self.move_variant == MoveVariant.EXCLUDED: - target_is_occupied = world.pop([target_0])[0] + target_is_occupied = world.pop([target_0])[0].value if target_is_occupied: print("Jump move: target turns out to be occupied.") target_0.is_entangled = False @@ -218,7 +218,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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] + target_is_occupied = world.pop([target_0])[0].value if target_is_occupied: print("Slide move not applied: target turns out to be occupied.") target_0.is_entangled = False @@ -228,7 +228,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: elif self.move_variant == MoveVariant.CAPTURE: could_capture = False if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: - if not world.pop(quantum_path_pieces_0)[0]: + if not world.pop(quantum_path_pieces_0)[0].value: quantum_path_pieces_0[0].reset() could_capture = True else: @@ -239,7 +239,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.quantum_if(*control_qubits).equals(*conditions).apply( alpha.Flip() )(capture_ancilla) - could_capture = world.pop([capture_ancilla])[0] + could_capture = world.pop([capture_ancilla])[0].value if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? @@ -438,12 +438,12 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 not world.pop([source_0])[0]: + if not world.pop([source_0])[0].value: source_0.reset() print("Cannonn fire not applied: source turns out to be empty.") return iter(()) # Target has to be there to fire. - if not world.pop([target_0])[0]: + if not world.pop([target_0])[0].value: target_0.reset() print("Cannonn fire not applied: target turns out to be empty.") return iter(()) @@ -453,7 +453,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: could_capture = False if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: # Consider this special case to save an ancilla. - if not world.pop(quantum_path_pieces_0)[0]: + if not world.pop(quantum_path_pieces_0)[0].value: quantum_path_pieces_0[0].reset() could_capture = True else: @@ -464,7 +464,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.quantum_if(*control_objects).equals(*conditions).apply( alpha.Flip() )(capture_ancilla) - could_capture = world.pop([capture_ancilla])[0] + could_capture = world.pop([capture_ancilla])[0].value if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? @@ -508,7 +508,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.quantum_if(*control_qubits).equals(*conditions).apply( alpha.Flip() )(capture_ancilla) - could_capture = world.pop([capture_ancilla])[0] + could_capture = world.pop([capture_ancilla])[0].value if could_capture: # Apply the capture. world.unhook(target_0) From 15c7cf156619f35bd85101d6329d7c73635948f4 Mon Sep 17 00:00:00 2001 From: madcpf Date: Wed, 18 Oct 2023 23:03:45 -0700 Subject: [PATCH 09/31] update --- .../examples/quantum_chinese_chess/chess.py | 4 - .../examples/quantum_chinese_chess/move.py | 119 +++++++++++++++--- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 90c43c12..17c0a156 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -368,10 +368,6 @@ def apply_move(self, str_to_parse: str) -> None: # King is captured, then the game is over. self.game_state = GameState(self.current_player) Jump(move_variant)(source_0, target_0) - # target_0.reset(source_0) - # source_0.reset() - # TODO(): only make such prints for a certain debug level. - # print("Classical move.") elif move_type == MoveType.JUMP: Jump(move_variant)(source_0, target_0) elif move_type == MoveType.SLIDE: diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 0a347073..98f52a42 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -152,6 +152,10 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: class SplitJump(QuantumEffect): + """SplitJump from source_0 to target_0 and target_1. The only accepted (default) move_variant is + - BASIC. + """ + def __init__( self, ): @@ -171,11 +175,14 @@ 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) - source_0.reset() return iter(()) class MergeJump(QuantumEffect): + """MergeJump from source_0 to source_1 to target_0. The only accepted (default) move_variant is + - BASIC. + """ + def __init__( self, ): @@ -199,6 +206,13 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 + """ + def __init__( self, quantum_path_pieces_0: List[str], @@ -219,6 +233,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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.") target_0.is_entangled = False @@ -229,16 +244,24 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: could_capture = False if not source_0.is_entangled and len(quantum_path_pieces_0) == 1: 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].value if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player @@ -248,6 +271,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ) return iter(()) # Apply the capture. + # Target qubit is unhooked, i.e. replaced with a new ancilla with value = 0. world.unhook(target_0) target_0.reset() alpha.PhasedMove()(source_0, target_0) @@ -258,20 +282,29 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: for path_piece in quantum_path_pieces_0: world.force_measurement(path_piece, 0) path_piece.reset() - # For BASIC or EXCLUDED cases + return iter(()) + # For BASIC or EXCLUDED cases. + # Note that we don't need to guarantee 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) here since there is a non-zero probability + # Note that we should not reset source_0 (to be empty) since there is a non-zero probability # that the source is not moved. 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 + """ + def __init__( self, quantum_path_pieces_0: List[str], @@ -289,6 +322,9 @@ def num_objects(self) -> Optional[int]: 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 ] @@ -299,23 +335,29 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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(). + # 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}") if len(quantum_path_pieces_0) == 0: + # If there is no quantum path piece in path 0, flip the ancilla. alpha.Flip()(path_0_clear_ancilla) else: + # Otherwise 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) - path_1_clear_ancilla = world._add_ancilla(f"{source_0.name}{target_1.name}") + # 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}") if len(quantum_path_pieces_1) == 0: + # If there is no quantum path piece in path 1, flip the ancilla. alpha.Flip()(path_1_clear_ancilla) else: + # Otherwise 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() @@ -340,7 +382,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: )(source_0, target_1) # TODO(): Do we need to zero-out, i.e. reverse those ancillas? - # Move the classical properties of the source piece to the target pieces. + + # 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 @@ -349,6 +392,12 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 + """ + def __init__( self, quantum_path_pieces_0: List[str], @@ -374,20 +423,25 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ] 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 split slide instead. + # 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) @@ -411,7 +465,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: )(source_1, target_0) # TODO(): Do we need to zero-out, i.e. reverse those ancillas? - # Move the classical properties of the source pieces to the target piece. + # 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. @@ -419,6 +473,12 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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. + """ + def __init__( self, classical_path_pieces_0: List[str], @@ -442,28 +502,35 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 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 cannon platform, the cannon could + # 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 not source_0.is_entangled and len(quantum_path_pieces_0) == 1: + if 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: quantum_path_pieces_0[0].reset() could_capture = True else: source_0.is_entangled = True + # 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].value if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player @@ -473,7 +540,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ) return iter(()) # Apply the capture. + # Quantumly reset the target. world.unhook(target_0) + # Classically reset the target. target_0.reset() alpha.PhasedMove()(source_0, target_0) # Move the classical properties of the source piece to the target piece. @@ -481,21 +550,29 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: source_0.reset() # Force measure all quantum_path_pieces_0 to be empty. for path_piece in quantum_path_pieces_0: - world.force_measurement(path_piece, 0) - path_piece.reset() + 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 occupied. + # one quantum path piece being occupied. could_capture = False source_0.is_entangled = True # TODO(): think a more efficient way of implementing this case. + # 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. 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( f"{expect_occupied_path_piece.name}" ) - expected_empty_pieces = [ + # 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 @@ -503,11 +580,14 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: control_qubits = [ source_0, expect_occupied_path_piece, - ] + expected_empty_pieces - conditions = [1, 1] + [0] * len(expected_empty_pieces) + ] + 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].value if could_capture: # Apply the capture. @@ -517,14 +597,15 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # Move the classical properties of the source piece to the target piece. target_0.reset(source_0) source_0.reset() - # Force measure all expected_empty_pieces to be empty. - for empty_path_piece in expected_empty_pieces: + # 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, 0) + 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: either the source turns out be empty, or there turns out to be (!=1) occupied path pieces." ) From 57971390d49175d71d583f296bb08cefb017b692 Mon Sep 17 00:00:00 2001 From: madcpf Date: Thu, 19 Oct 2023 09:48:10 -0700 Subject: [PATCH 10/31] up --- .../examples/quantum_chinese_chess/board.py | 8 +- .../examples/quantum_chinese_chess/move.py | 97 ++++-- .../quantum_chinese_chess/move_test.py | 312 +++++++++++++++--- .../quantum_chinese_chess/test_utils.py | 188 ++++++++++- 4 files changed, 517 insertions(+), 88 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index ca33f3a2..4c2612fc 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -63,7 +63,7 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": FEN rule for Chinese Chess could be found at https://www.wxf-xiangqi.org/images/computer-xiangqi/fen-for-xiangqi-chinese-chess.pdf """ chess_board = {} - row_index = 9 + row_index = 0 king_locations = [] pieces, turns = fen.split(" ", 1) for row in pieces.split("/"): @@ -88,15 +88,11 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": name, SquareState.OCCUPIED, piece_type, color ) col += 1 - row_index -= 1 + row_index += 1 board = alpha.QuantumWorld(chess_board.values()) # Here 0 means the player RED while 1 the player BLACK. current_player = 0 if "w" in turns else 1 # TODO(): maybe add check to make sure the input fen itself is correct. - if len(king_locations) != 2: - raise ValueError( - f"We expect two KINGs on the board, but got {len(king_locations)}." - ) return cls(board, current_player, king_locations) def to_str(self, probs: List[float] = None): diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 98f52a42..760efe63 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -27,27 +27,33 @@ class Move(QuantumEffect): def __init__( self, - source_0: Piece, - target_0: Piece, + source: str, + target: str, board: Board, - move_type: MoveType, - move_variant: MoveVariant, - source_1: Piece = None, - target_1: Piece = None, + source2: Optional[str] = None, + target2: Optional[str] = None, + move_type: Optional[MoveType] = None, + move_variant: Optional[MoveVariant] = None, ): - self.source_0 = source_0 - self.source_1 = source_1 - self.target_0 = target_0 - self.target_1 = target_1 + self.source = source + self.source2 = source2 + self.target = target + self.target2 = target2 self.move_type = move_type self.move_variant = move_variant self.board = board def __eq__(self, other): - return self.to_str(3) == other.to_str(3) - - def num_dimension(self) -> Optional[int]: - return 2 + if isinstance(other, Move): + return ( + self.source == other.source + and self.source2 == other.source2 + and self.target == other.target + and self.target2 == other.target2 + and self.move_type == other.move_type + and self.move_variant == other.move_variant + ) + return False def _verify_objects(self, *objects): # TODO(): add checks that apply to all move types @@ -58,10 +64,10 @@ def effect(self, *objects): return def is_split_move(self) -> bool: - return self.target_1 is not None + return self.target2 is not None def is_merge_move(self) -> bool: - return self.source_1 is not None + return self.source2 is not None def to_str(self, verbose_level: int = 1) -> str: """Constructs the string representation of the move. @@ -74,29 +80,27 @@ def to_str(self, verbose_level: int = 1) -> str: return "" if self.is_split_move(): - move_str = [ - self.source_0.name + "^" + self.target_0.name + self.target_1.name - ] + move_str = [self.source + "^" + self.target + str(self.target2)] elif self.is_merge_move(): - move_str = [ - self.source_0.name + self.source_1.name + "^" + self.target_0.name - ] + move_str = [self.source + str(self.source2) + "^" + self.target] else: - move_str = [self.source_0.name + self.target_0.name] + move_str = [self.source + self.target] if verbose_level > 1: move_str.append(self.move_type.name) move_str.append(self.move_variant.name) if verbose_level > 2: + source = self.board.board[self.source] + target = self.board.board[self.target] move_str.append( - self.source_0.color.name + source.color.name + "_" - + self.source_0.type_.name + + source.type_.name + "->" - + self.target_0.color.name + + target.color.name + "_" - + self.target_0.type_.name + + target.type_.name ) return ":".join(move_str) @@ -105,6 +109,13 @@ def __str__(self): class Jump(QuantumEffect): + """Jump from source_0 to target_0. The accepted move_variant includes + - CLASSICAL (where all classical moves will be handled here) + - CAPTURE + - EXCLUDED + - BASIC + """ + def __init__( self, move_variant: MoveVariant, @@ -118,32 +129,48 @@ def num_objects(self) -> Optional[int]: return 2 def effect(self, *objects) -> Iterator[cirq.Operation]: - # TODO(): currently pawn capture is a same as jump capture, while in quantum chess it's different, + # TODO(): currently pawn capture is the same as jump capture, while in quantum chess it's different, # i.e. pawn would move only if the target is there, i.e. CNOT(t, s), and an entanglement could be # created. This could be a general game setting, i.e. we could allow players to choose if they # want the source piece to move (in case of capture) if the target piece is not there. source_0, target_0 = objects world = source_0.world if self.move_variant == MoveVariant.CAPTURE: + # We peek and force measure source_0. source_is_occupied = world.pop([source_0])[0].value + # For move_variant==CAPTURE, we require source_0 to be occupied before further actions. + # This is to prevent a piece of the board containing two types of different pieces. if not source_is_occupied: + # If source_0 turns out to be not there, we set it to be EMPTY, and the jump + # could not be made. source_0.reset() - print("Jump move: source turns out to be empty.") + print("Jump move not applied: source turns out to be empty.") return iter(()) source_0.is_entangled = False + # We replace the qubit of target_0 with a new ancilla, and set its classical properties to be EMPTY. world.unhook(target_0) target_0.reset() elif self.move_variant == MoveVariant.EXCLUDED: + # We peek and force measure target_0. target_is_occupied = world.pop([target_0])[0].value + # For move_variant==EXCLUDED, we require target_0 to be empty before further actions. + # This is to prevent a piece of the board containing two types of different pieces. if target_is_occupied: - print("Jump move: target turns out to be occupied.") + # If target_0 turns out to be there, we set it to be classically OCCUPIED, and + # the jump could not be made. + print("Jump move not applied: target turns out to be occupied.") target_0.is_entangled = False return iter(()) + # Otherwise we set target_0 to be classically EMPTY. target_0.reset() elif self.move_variant == MoveVariant.CLASSICAL: - world.unhook(target_0) - target_0.reset() + if target_0.type_ != Type.EMPTY: + # For classical moves with target_0 occupied, we replace the qubit of target_0 with + # a new ancilla, and set its classical properties to be EMPTY. + world.unhook(target_0) + target_0.reset() + # Make the jump move. alpha.PhasedMove()(source_0, target_0) # Move the classical properties of the source piece to the target piece. target_0.reset(source_0) @@ -197,9 +224,9 @@ def num_objects(self) -> Optional[int]: def effect(self, *objects) -> Iterator[cirq.Operation]: source_0, source_1, target_0 = objects # Make the merge jump. - cirq.ISWAP(source_0.qubit, target_0.qubit) ** -0.5 - cirq.ISWAP(source_0.qubit, target_0.qubit) ** -0.5 - cirq.ISWAP(source_1.qubit, target_0.qubit) ** -0.5 + alpha.PhasedMove(-0.5)(source_0, target_0) + alpha.PhasedMove(-0.5)(source_0, target_0) + alpha.PhasedMove(-0.5)(source_1, target_0) # Pass the classical properties of the source pieces to the target piece. target_0.reset(source_0) return iter(()) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index c90b201a..205f9095 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -11,27 +11,71 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from unitary.examples.quantum_chinese_chess.move import Move +from unitary.examples.quantum_chinese_chess.move import ( + Move, + Jump, + SplitJump, + MergeJump, + Slide, + SplitSlide, + MergeSlide, + CannonFire, +) from unitary.examples.quantum_chinese_chess.board import Board -from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant +from unitary.examples.quantum_chinese_chess.piece import Piece import pytest -from string import ascii_lowercase, digits - - -def global_names(): - global board - board = Board.from_fen() - for col in ascii_lowercase[:9]: - for row in digits: - globals()[f"{col}{row}"] = board.board[f"{col}{row}"] +from unitary import alpha +from typing import List +from unitary.examples.quantum_chinese_chess.enums import ( + MoveType, + MoveVariant, + SquareState, + Type, + Color, +) +from unitary.examples.quantum_chinese_chess.test_utils import ( + locations_to_bitboard, + assert_samples_in, + assert_sample_distribution, + assert_this_or_that, + assert_prob_about, + assert_fifty_fifty, + sample_board, + get_board_probability_distribution, + print_samples, + set_board, +) def test_move_eq(): - global_names() - move1 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) - move2 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) - move3 = Move(a1, b2, board, MoveType.JUMP, MoveVariant.CAPTURE) - move4 = Move(a1, b2, board, MoveType.MERGE_SLIDE, MoveVariant.CAPTURE, c1) + board = Board.from_fen() + move1 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + move2 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + move3 = Move( + "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE + ) + move4 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_SLIDE, + move_variant=MoveVariant.CAPTURE, + ) assert move1 == move2 assert move1 != move3 @@ -40,35 +84,227 @@ def test_move_eq(): def test_move_type(): # TODO(): change to real senarios - move1 = Move(a1, b2, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) + board = Board.from_fen() + move1 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) assert move1.is_split_move() == False assert move1.is_merge_move() - move2 = Move(a1, b2, board, MoveType.SPLIT_JUMP, MoveVariant.BASIC, target_1=c1) + move2 = Move( + "a1", + "b2", + board, + target2="c1", + move_type=MoveType.SPLIT_JUMP, + move_variant=MoveVariant.BASIC, + ) assert move2.is_split_move() assert move2.is_merge_move() == False - move3 = Move(a1, b2, board, MoveType.SLIDE, MoveVariant.CAPTURE) + move3 = Move( + "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE + ) assert move3.is_split_move() == False assert move3.is_merge_move() == False -def test_to_str(): - # TODO(): change to real scenarios - move1 = Move(a0, a6, board, MoveType.MERGE_JUMP, MoveVariant.CAPTURE, c1) - assert move1.to_str(0) == "" - assert move1.to_str(1) == "a0c1^a6" - assert move1.to_str(2) == "a0c1^a6:MERGE_JUMP:CAPTURE" - assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:BLACK_ROOK->RED_PAWN" - - move2 = Move(a0, b3, board, MoveType.SPLIT_JUMP, MoveVariant.BASIC, target_1=c1) - assert move2.to_str(0) == "" - assert move2.to_str(1) == "a0^b3c1" - assert move2.to_str(2) == "a0^b3c1:SPLIT_JUMP:BASIC" - assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:BLACK_ROOK->NA_EMPTY" - - move3 = Move(a0, a6, board, MoveType.SLIDE, MoveVariant.CAPTURE) - assert move3.to_str(0) == "" - assert move3.to_str(1) == "a0a6" - assert move3.to_str(2) == "a0a6:SLIDE:CAPTURE" - assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:BLACK_ROOK->RED_PAWN" +# def test_to_str(): +# # TODO(): change to real scenarios +# board = Board.from_fen() +# move1 = Move( +# "a0", +# "a6", +# board, +# "c1", +# move_type=MoveType.MERGE_JUMP, +# move_variant=MoveVariant.CAPTURE, +# ) +# assert move1.to_str(0) == "" +# assert move1.to_str(1) == "a0c1^a6" +# assert move1.to_str(2) == "a0c1^a6:MERGE_JUMP:CAPTURE" +# assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:RED_ROOK->BLACK_PAWN" + +# move2 = Move( +# "a0", +# "b3", +# board, +# target2="c1", +# move_type=MoveType.SPLIT_JUMP, +# move_variant=MoveVariant.BASIC, +# ) +# assert move2.to_str(0) == "" +# assert move2.to_str(1) == "a0^b3c1" +# assert move2.to_str(2) == "a0^b3c1:SPLIT_JUMP:BASIC" +# assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:RED_ROOK->NA_EMPTY" + +# move3 = Move( +# "a0", "a6", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE +# ) +# assert move3.to_str(0) == "" +# assert move3.to_str(1) == "a0a6" +# assert move3.to_str(2) == "a0a6:SLIDE:CAPTURE" +# assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:RED_ROOK->BLACK_PAWN" + + +# def test_jump_classical(): +# # Target is empty. +# board = set_board(["a1", "b1"]) +# world = board.board +# # TODO(): try move all varaibles declarations of a1 = world["a1"] into a function. +# Jump(MoveVariant.CLASSICAL)(world["a1"], world["b2"]) +# assert_samples_in(board, [locations_to_bitboard(["b2", "b1"])]) + +# # Target is occupied. +# Jump(MoveVariant.CLASSICAL)(world["b2"], world["b1"]) +# assert_samples_in(board, [locations_to_bitboard(["b1"])]) + + +# def test_jump_capture(): +# # Source is in quantum state. +# board = set_board(["a1", "b1"]) +# world = board.board +# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2", "b1"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b1"])) +# Jump(MoveVariant.CAPTURE)(world["a2"], world["b1"]) +# # pop() will break the supersition and only one of the following two states are possible. +# # We check the ancilla to learn if the jump was applied or not. +# source_is_occupied = world.post_selection[world["ancilla_a2_0"]] +# if source_is_occupied: +# assert_samples_in(board, [locations_to_bitboard(["b1"])]) +# else: +# assert_samples_in(board, [locations_to_bitboard(["a3", "b1"])]) + +# # Target is in quantum state. +# board = set_board(["a1", "b1"]) +# world = board.board +# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) +# Jump(MoveVariant.CAPTURE)(world["a1"], world["b2"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) + +# # Both source and target are in quantum state. +# board = set_board(["a1", "b1"]) +# world = board.board +# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) +# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a2", "b2"]): 1 / 4.0, +# locations_to_bitboard(["a2", "b3"]): 1 / 4.0, +# locations_to_bitboard(["a3", "b2"]): 1 / 4.0, +# locations_to_bitboard(["a3", "b3"]): 1 / 4.0, +# }, +# ) +# Jump(MoveVariant.CAPTURE)(world["a2"], world["b2"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# # We check the ancilla to learn if the jump was applied or not. +# source_is_occupied = world.post_selection[world["ancilla_a2_0"]] +# print(source_is_occupied) +# if source_is_occupied: +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) +# else: +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b2"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b3"])) + + +# def test_jump_excluded(): +# # Target is in quantum state. +# board = set_board(["a1", "b1"]) +# world = board.board +# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) +# Jump(MoveVariant.EXCLUDED)(world["a1"], world["b2"]) +# # pop() will break the supersition and only one of the following two states are possible. +# # We check the ancilla to learn if the jump was applied or not. +# target_is_occupied = world.post_selection[world["ancilla_b2_0"]] +# print(target_is_occupied) +# if target_is_occupied: +# assert_samples_in(board, [locations_to_bitboard(["a1", "b2"])]) +# else: +# assert_samples_in(board, [locations_to_bitboard(["b2", "b3"])]) + +# # Both source and target are in quantum state. +# board = set_board(["a1", "b1"]) +# world = board.board +# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) +# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) +# Jump(MoveVariant.EXCLUDED)(world["a2"], world["b2"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# # We check the ancilla to learn if the jump was applied or not. +# target_is_occupied = world.post_selection[world["ancilla_b2_0"]] +# print(target_is_occupied) +# if target_is_occupied: +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2", "b2"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b2"])) +# else: +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b3"])) + + +# def test_jump_basic(): +# # Source is in quantum state. +# board = set_board(["a1"]) +# world = board.board +# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) +# Jump(MoveVariant.BASIC)(world["a2"], world["d1"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["d1"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + + +def test_split_jump(): + # Source is in classical state. + board = set_board(["a1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + board_probabilities = get_board_probability_distribution(board, 1000) + assert len(board_probabilities) == 2 + assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) + assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + + # Source is in quantum state. + board = set_board(["a1"]) + world = board.board + alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["a3"], world["a4"], world["a5"]) + samples = sample_board(board, 100) + assert_sample_distribution( + board, + { + locations_to_bitboard(["a2"]): 0.5, + locations_to_bitboard(["a4"]): 0.25, + locations_to_bitboard(["a5"]): 0.25, + }, + ) + + +def test_merge_jump(): + # Two quantum pieces split from one source could be merge back to one. + board = set_board(["a1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + MergeJump()(world["a2"], world["a3"], world["a1"]) + assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + + board = set_board(["a1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["a3"], world["a4"], world["a5"]) + + MergeJump()(world["a2"], world["a3"], world["a1"]) + assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) diff --git a/unitary/examples/quantum_chinese_chess/test_utils.py b/unitary/examples/quantum_chinese_chess/test_utils.py index 98184187..1c467a0b 100644 --- a/unitary/examples/quantum_chinese_chess/test_utils.py +++ b/unitary/examples/quantum_chinese_chess/test_utils.py @@ -12,14 +12,184 @@ # See the License for the specific language governing permissions and # limitations under the License. from unitary.alpha import QuantumObject, QuantumWorld -from unitary.examples.quantum_chinese_chess.enums import SquareState -from string import ascii_lowercase, digits +from unitary.examples.quantum_chinese_chess.enums import SquareState, Type, Color +from unitary.examples.quantum_chinese_chess.board import Board +from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary import alpha +from typing import List, Dict +from collections import defaultdict +from scipy.stats import chisquare -# Build quantum objects a0 to i9, and add them to a quantum world. -def init_board() -> QuantumWorld: - board = {} - for col in ascii_lowercase[:9]: - for row in digits: - board[col + row] = QuantumObject(col + row, SquareState.EMPTY) - return QuantumWorld(list(board.values())) +_EMPTY_FEN = "9/9/9/9/9/9/9/9/9/9 w---1" + + +def set_board(positions: List[str]) -> Board: + """Returns a board with the specified positions filled with + RED ROOKs. + """ + board = Board.from_fen(_EMPTY_FEN) + for position in positions: + board.board[position].reset( + Piece(position, SquareState.OCCUPIED, Type.ROOK, Color.RED) + ) + alpha.Flip()(board.board[position]) + return board + + +def location_to_bit(location: str) -> int: + """Transform location notation (e.g. "a3") into a bitboard bit number. + The return value ranges from 0 to 89. + """ + x = ord(location[0]) - ord("a") + y = int(location[1]) + return y * 9 + x + + +def locations_to_bitboard(locations: List[str]) -> int: + """Transform a list of locations into a 90-bit board bitstring. + Each nonzero bit of the bitstring indicates that the corresponding + piece is occupied. + """ + bitboard = 0 + for location in locations: + bitboard += 1 << location_to_bit(location) + return bitboard + + +def nth_bit_of(n: int, bit_board: int) -> bool: + """Returns the `n`-th (zero-based) bit of a 90-bit bitstring `bit_board`.""" + return (bit_board >> n) % 2 == 1 + + +def bit_to_location(bit: int) -> str: + """Transform a bitboard bit number into a location (e.g. "a3").""" + y = bit // 9 + x = chr(bit % 9 + ord("a")) + return f"{x}{y}" + + +def bitboard_to_locations(bitboard: int) -> List[str]: + """Transform a 90-bit bitstring `bitboard` into a list of locations.""" + locations = [] + for n in range(90): + if nth_bit_of(n, bitboard): + locations.append(bit_to_location(n)) + return locations + + +def sample_board(board: Board, repetitions: int) -> List[int]: + """Sample the given `board` by the given `repetitions`. + Returns a list of 90-bit bitstring, each corresponding to one sample. + """ + samples = board.board.peek(count=repetitions, convert_to_enum=False) + # Convert peek results (in List[List[int]]) into List[int]. + samples = [ + int("0b" + "".join([str(i) for i in sample[::-1]]), base=2) + for sample in samples + ] + return samples + + +def print_samples(samples: List[int]) -> None: + """Aggregate all the samples and print the dictionary of {locations: count}.""" + sample_dict = {} + for sample in samples: + if sample not in sample_dict: + sample_dict[sample] = 0 + sample_dict[sample] += 1 + print("Actual samples:") + for key in sample_dict: + print(f"{bitboard_to_locations(key)}: {sample_dict[key]}") + + +def get_board_probability_distribution( + board: Board, repetitions: int = 1000 +) -> Dict[int, float]: + """Returns the probability distribution for each board found in the sample. + The values are returned as a dict{bitboard(int): probability(float)}. + """ + board_probabilities: Dict[int, float] = {} + + samples = sample_board(board, repetitions) + for sample in samples: + if sample not in board_probabilities: + board_probabilities[sample] = 0.0 + board_probabilities[sample] += 1.0 + + for board in board_probabilities: + board_probabilities[board] /= repetitions + + return board_probabilities + + +def assert_samples_in(board: Board, probabilities: Dict[int, float]) -> None: + """Samples the given `board` and asserts that all samples are within + the given `probabilities` (i.e. a map from bitstring into its possibility), + and that each possibility is represented at least once in the samples. + """ + samples = sample_board(board, 500) + assert len(samples) == 500 + all_in = all(sample in probabilities for sample in samples) + assert all_in, print_samples(samples) + # Make sure each possibility is represented at least once. + for possibility in probabilities: + any_in = any(sample == possibility for sample in samples) + assert any_in, print_samples(samples) + + +def assert_sample_distribution( + board: Board, probabilities: Dict[int, float], p_significant: float = 1e-6 +) -> None: + """Performs a chi-squared test that samples follow an expected distribution. + `probabilities` is a map from bitboards to expected probability. An + AssertionError is raised if any of the samples is not in the map, or if the + expected versus observed samples fails the chi-squared test. + """ + n_samples = 500 + assert abs(sum(probabilities.values()) - 1) < 1e-9 + samples = sample_board(board, n_samples) + counts = defaultdict(int) + for sample in samples: + assert sample in probabilities, bitboard_to_locations(sample) + counts[sample] += 1 + observed = [] + expected = [] + for position, probability in probabilities.items(): + observed.append(counts[position]) + expected.append(n_samples * probability) + p = chisquare(observed, expected).pvalue + assert ( + p > p_significant + ), f"Observed {observed} is far from expected {expected} (p = {p})" + + +def assert_this_or_that(samples: List[int], this: int, that: int) -> None: + """Asserts all the samples are either equal to `this` or `that`, + and that at least one of them exists in the samples. + """ + assert any(sample == this for sample in samples), print_samples(samples) + assert any(sample == that for sample in samples), print_samples(samples) + assert all(sample == this or sample == that for sample in samples), print_samples( + samples + ) + + +def assert_prob_about( + probabilities: Dict[int, float], that: int, expected: float, atol: float = 0.05 +) -> None: + """Checks that the probability of `that` is within `atol` of the value of `expected`.""" + assert that in probabilities, print_samples(list(probabilities.keys())) + assert probabilities[that] > expected - atol, print_samples( + list(probabilities.keys()) + ) + assert probabilities[that] < expected + atol, print_samples( + list(probabilities.keys()) + ) + + +def assert_fifty_fifty(probabilities, that): + """Checks that the probability of `that` is close to 50%.""" + assert_prob_about(probabilities, that, 0.5), print_samples( + list(probabilities.keys()) + ) From bf43fef16cd3f554c01c72a3788487cfac35d8e1 Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Thu, 19 Oct 2023 14:58:46 -0700 Subject: [PATCH 11/31] test --- unitary/alpha/quantum_world.py | 11 +- .../examples/quantum_chinese_chess/move.py | 5 +- .../quantum_chinese_chess/move_test.py | 419 ++++++++++++++++-- .../quantum_chinese_chess/test_utils.py | 2 +- 4 files changed, 403 insertions(+), 34 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 7d008d54..fe777fa7 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -316,6 +316,7 @@ def force_measurement( state of the result. """ new_obj = self._add_ancilla(namespace=obj.name, value=result) + print(new_obj.name) # 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: @@ -506,10 +507,14 @@ def get_binary_probabilities( return binary_probs def __getitem__(self, name: str) -> QuantumObject: - quantum_object = self.object_name_dict.get(name, None) - if not quantum_object: + try: + quantum_object = self.object_name_dict.get(name, None) + return quantum_object + except: + print("exsiting") + for obj in self.object_name_dict.keys(): + print(obj) raise KeyError(f"{name} did not exist in this world.") - return quantum_object def unhook(self, object: QuantumObject) -> None: new_ancilla = self._add_ancilla(object.name) diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 760efe63..3a122ed9 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -289,7 +289,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.Flip() )(capture_ancilla) # We measure the ancilla to dertermine whether the slide could be made. - could_capture = world.pop([capture_ancilla])[0].value + could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? @@ -298,6 +298,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: ) 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. replaced with a new ancilla with value = 0. world.unhook(target_0) target_0.reset() diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 205f9095..3e7b9a72 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -267,44 +267,405 @@ def test_move_type(): # assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) -def test_split_jump(): - # Source is in classical state. - board = set_board(["a1"]) - world = board.board - SplitJump()(world["a1"], world["a2"], world["a3"]) - board_probabilities = get_board_probability_distribution(board, 1000) - assert len(board_probabilities) == 2 - assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) - assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) +# def test_split_jump(): +# # Source is in classical state. +# board = set_board(["a1"]) +# world = board.board +# SplitJump()(world["a1"], world["a2"], world["a3"]) +# board_probabilities = get_board_probability_distribution(board, 1000) +# assert len(board_probabilities) == 2 +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) +# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + +# # Source is in quantum state. +# board = set_board(["a1"]) +# world = board.board +# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) +# SplitJump()(world["a3"], world["a4"], world["a5"]) +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a2"]): 0.5, +# locations_to_bitboard(["a4"]): 0.25, +# locations_to_bitboard(["a5"]): 0.25, +# }, +# ) + + +# def test_merge_jump(): +# # Two quantum pieces split from one source could be merge back to one. +# board = set_board(["a1"]) +# world = board.board +# SplitJump()(world["a1"], world["a2"], world["a3"]) +# MergeJump()(world["a2"], world["a3"], world["a1"]) +# assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + +# # Imperfect merge scenario 1 +# board = set_board(["a1"]) +# world = board.board +# SplitJump()(world["a1"], world["a2"], world["a3"]) +# SplitJump()(world["a3"], world["a4"], world["a5"]) +# # a2 has prob. 0.5 to be occupied, while a4 has prob. 0.25 to be occupied +# MergeJump()(world["a2"], world["a4"], world["a6"]) +# # Accoding to matrix calculations, the ending coefficient of +# # a5 to be occupied: -1/2; +# # a6 to be occupied: 1/2 + i/2/sqrt(2) +# # a4 to be occupied: -i/2 -1/2/sqrt(2) +# # a2 to be occupied: 0 +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a5"]): 1./4, +# locations_to_bitboard(["a6"]): 3./8, +# locations_to_bitboard(["a4"]): 3./8, +# }, +# ) + +# # Imperfect merge scenario 2 +# # Two quantum pieces split from two sources could not be merge into to one. +# board = set_board(["a1", "b1"]) +# world = board.board +# SplitJump()(world["a1"], world["a2"], world["a3"]) +# SplitJump()(world["b1"], world["b2"], world["b3"]) +# MergeJump()(world["a2"], world["b2"], world["c2"]) +# # According to matrix calculations, the ending coefficient of +# # [a3, b3]: 1/4 +# # [a3, c2]: i/2/sqrt(2) +# # [a3, b2]: -1/2/sqrt(2) +# # [b3, c2]: i/2/sqrt(2) +# # [b2, b3]: 1/2/sqrt(2) +# # [b2, c2]: 1/4 +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a3", "b3"]): 1./4, +# locations_to_bitboard(["a3", "c2"]): 1./8, +# locations_to_bitboard(["a3", "b2"]): 1./8, +# locations_to_bitboard(["b3", "c2"]): 1./8, +# locations_to_bitboard(["b2", "b3"]): 1./8, +# locations_to_bitboard(["b2", "c2"]): 1./4, +# }, +# ) + +# # Imperfect merge scenario 3 +# # This is a simplied version of the scenario above, where we unhook a3 and b3. +# board = set_board(["a1", "b1"]) +# world = board.board +# SplitJump()(world["a1"], world["a2"], world["a3"]) +# SplitJump()(world["b1"], world["b2"], world["b3"]) +# board.board.unhook(world["a3"]) +# board.board.unhook(world["b3"]) +# # Now the only quantum pieces in the board are a2 and b2. +# MergeJump()(world["a2"], world["b2"], world["c2"]) +# # The expected distribution is same as above by summing over a3 and b3. +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard([]): 1./4, +# locations_to_bitboard(["c2"]): 1./4, +# locations_to_bitboard(["b2"]): 1./4, +# locations_to_bitboard(["b2", "c2"]): 1./4, +# }, +# ) + + +# def test_slide_basic(): +# # 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./2, +# locations_to_bitboard(["b3", "c1"]): 1./2, # success +# }, +# ) + +# # 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"]) - # Source is in quantum state. - board = set_board(["a1"]) +# Slide(["b2"], MoveVariant.EXCLUDED)(world["a2"], world["c1"]) + +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a2", "b2"]): 1./4, +# locations_to_bitboard(["a3", "b2"]): 1./4, +# locations_to_bitboard(["c1", "b3"]): 1./4, # success +# locations_to_bitboard(["a3", "b3"]): 1./4, +# }, +# ) + +# # 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./8, +# locations_to_bitboard(["a2", "b2", "c3"]): 1./8, +# locations_to_bitboard(["a2", "b3", "c2"]): 1./8, +# locations_to_bitboard(["d1", "b3", "c3"]): 1./8, # success +# locations_to_bitboard(["a3", "b2", "c2"]): 1./8, +# locations_to_bitboard(["a3", "b2", "c3"]): 1./8, +# locations_to_bitboard(["a3", "b3", "c2"]): 1./8, +# locations_to_bitboard(["a3", "b3", "c3"]): 1./8, +# }, +# ) + + +# def test_slide_excluded(): +# # 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./2, +# locations_to_bitboard(["a1", "b3", "c2"]): 1./2, +# }, +# ) +# else: +# # a1 could move to c2 if b2 is not there. +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a1", "b2", "c3"]): 1./2, +# locations_to_bitboard(["b3", "c2", "c3"]): 1./2, # success +# }, +# ) + +# # 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./4, +# locations_to_bitboard(["a3", "b3", "c2"]): 1./4, +# locations_to_bitboard(["a2", "b2", "c2"]): 1./4, +# locations_to_bitboard(["a2", "b3", "c2"]): 1./4, +# }, +# ) +# else: +# # a2 could move to c2 if b2 is not there. +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["a2", "b2", "c3"]): 1./4, +# locations_to_bitboard(["a3", "b2", "c3"]): 1./4, +# locations_to_bitboard(["b3", "c2", "c3"]): 1./4, # success +# locations_to_bitboard(["a3", "b3", "c3"]): 1./4, +# }, +# ) + +# # 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./8, +# locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1./8, +# locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1./8, +# locations_to_bitboard(["a2", "b3", "c3", "d2"]): 1./8, +# locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1./8, +# locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1./8, +# locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1./8, +# locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1./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./8, +# locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1./8, +# locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1./8, +# locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1./8, +# locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1./8, +# locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1./8, +# locations_to_bitboard(["d2", "b3", "c3", "d3"]): 1./8, # success +# locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1./8, +# }, +# ) + + +# def test_slide_capture(): +# # 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./2, +# locations_to_bitboard(["a1", "b2", "c3"]): 1./2, +# }, +# ) +# else: +# # a1 moves to c2. +# assert_sample_distribution( +# board, +# { +# locations_to_bitboard(["b3", "c2"]): 1./2, # slided and captured +# locations_to_bitboard(["b3", "c2", "c3"]): 1./2, # slided but not captured +# }, +# ) + +# # 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", "c2", "d2"]): 1./2, # slided and captured +# locations_to_bitboard(["b3", "c2", "d2", "d3"]): 1./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./14, +# locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1./14, +# locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1./14, +# locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1./14, +# locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1./14, +# locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1./14, +# locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1./14, +# locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1./14, +# locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1./14, +# locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1./14, +# locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1./14, +# locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1./14, +# # cases where the source is not there +# locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1./14, +# locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1./14, +# }, +# ) + + +def test_split_slide(): + # Source is in classical state + one arm is path-clear. + board = set_board(["a1", "b1"]) world = board.board - alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) - SplitJump()(world["a3"], world["a4"], world["a5"]) - samples = sample_board(board, 100) + SplitJump()(world["b1"], world["b2"], world["b3"]) + + SplitSlide(["b2"], [])(world["a1"], world["c1"], world["c2"]) + assert_sample_distribution( board, { - locations_to_bitboard(["a2"]): 0.5, - locations_to_bitboard(["a4"]): 0.25, - locations_to_bitboard(["a5"]): 0.25, + 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_merge_jump(): - # Two quantum pieces split from one source could be merge back to one. - board = set_board(["a1"]) + # Source in quantum state + multiple path qubits. + board = set_board(["a1", "b1", "c1", "d1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"]) - MergeJump()(world["a2"], world["a3"], world["a1"]) - assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + SplitJump()(world["b1"], world["b2"], world["b3"]) + SplitJump()(world["c1"], world["c2"], world["c3"]) + SplitJump()(world["d1"], world["d2"], world["d3"]) - board = set_board(["a1"]) - world = board.board - SplitJump()(world["a1"], world["a2"], world["a3"]) - SplitJump()(world["a3"], world["a4"], world["a5"]) + 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 / 17, + locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 17, + locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 17, + locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 17, + locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 17, + locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 17, + # path 0 is clear + locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1.0 / 17, + locations_to_bitboard(["e1", "b3", "c3", "d2"]): 1.0 / 17, # slide to e1 + # path 1 is clear + locations_to_bitboard(["e2", "b2", "c2", "d3"]): 1.0 / 17, # slide to e2 + locations_to_bitboard(["e2", "b3", "c2", "d3"]): 1.0 / 17, # slide to e2 + locations_to_bitboard(["e2", "b2", "c3", "d3"]): 1.0 / 17, # slide to e2 + locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 17, + locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 17, + locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 17, + # both paths are clear + locations_to_bitboard(["e1", "b3", "c3", "d3"]): 1.0 / 17, # slide to e1 + locations_to_bitboard(["e2", "b3", "c3", "d3"]): 1.0 / 17, # slide to e2 + locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 17, + }, + ) - MergeJump()(world["a2"], world["a3"], world["a1"]) - assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + # Source in quantum state + overlapped paths. diff --git a/unitary/examples/quantum_chinese_chess/test_utils.py b/unitary/examples/quantum_chinese_chess/test_utils.py index 1c467a0b..8e0f82e8 100644 --- a/unitary/examples/quantum_chinese_chess/test_utils.py +++ b/unitary/examples/quantum_chinese_chess/test_utils.py @@ -151,7 +151,7 @@ def assert_sample_distribution( samples = sample_board(board, n_samples) counts = defaultdict(int) for sample in samples: - assert sample in probabilities, bitboard_to_locations(sample) + assert sample in probabilities, print_samples(samples) counts[sample] += 1 observed = [] expected = [] From 366edee5e4b88e94a8dfa2575ff47f946c37066d Mon Sep 17 00:00:00 2001 From: madcpf Date: Thu, 19 Oct 2023 21:47:53 -0700 Subject: [PATCH 12/31] update --- .../examples/quantum_chinese_chess/board.py | 11 +- .../examples/quantum_chinese_chess/enums.py | 4 +- .../quantum_chinese_chess/enums_test.py | 10 +- .../examples/quantum_chinese_chess/move.py | 186 +++++----- .../quantum_chinese_chess/move_test.py | 331 ++++++++++++++++-- .../quantum_chinese_chess/piece_test.py | 6 +- 6 files changed, 410 insertions(+), 138 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index 4c2612fc..ed1ab2e4 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -19,8 +19,10 @@ Color, Type, Language, + MoveVariant, ) from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.move import * reset = "\033[0m" bold = "\033[01m" @@ -152,7 +154,7 @@ def to_str(self, probs: List[float] = None): # We need to turn letters into their full-width counterparts to align # a mix of letters + Chinese characters. half_width_chars = "".join( - [" ", "\uFF65"] + [" ", "・"] + [chr(c) for c in range(ord("A"), ord("Z"))] + [chr(c) for c in range(ord("a"), ord("z"))] ) @@ -244,18 +246,17 @@ def flying_general_check(self) -> bool: alpha.quantum_if(*control_objects).equals(*conditions).apply(alpha.Flip())( capture_ancilla ) - could_capture = self.board.pop([capture_ancilla])[0].value + could_capture = self.board.pop([capture_ancilla])[0] if could_capture: # Force measure all path pieces to be empty. for path_piece in control_objects: self.board.force_measurement(path_piece, 0) path_piece.reset() - # Let the general/king fly. + # Let the general/king fly, i.e. the opposite king will capture the current king. current_king = self.board[self.king_locations[self.current_player]] oppsite_king = self.board[self.king_locations[1 - self.current_player]] - current_king.reset(oppsite_king) - oppsite_king.reset() + Jump(MoveVariant.CLASSICAL)(oppsite_king, current_king) print("==== FLYING GENERAL ! ====") return True else: diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index f7f44112..6341a409 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -85,7 +85,7 @@ class Type(enum.Enum): - Chinese black """ - EMPTY = ("\uFF65", "\uFF65", "\uFF65", "\uFF65") + EMPTY = ("・", "・", "・", "・") PAWN = ("P", "p", "兵", "卒") CANNON = ("C", "c", "炮", "砲") ROOK = ("R", "r", "车", "車") @@ -104,7 +104,7 @@ def type_of(c: str) -> Optional["Type"]: "e": Type.ELEPHANT, "a": Type.ADVISOR, "k": Type.KING, - ".": Type.EMPTY, + "・": Type.EMPTY, }.get(c.lower(), None) @staticmethod diff --git a/unitary/examples/quantum_chinese_chess/enums_test.py b/unitary/examples/quantum_chinese_chess/enums_test.py index 894a4991..7fa8cb2e 100644 --- a/unitary/examples/quantum_chinese_chess/enums_test.py +++ b/unitary/examples/quantum_chinese_chess/enums_test.py @@ -19,7 +19,7 @@ def test_type_of(): assert Type.type_of("P") == Type.PAWN assert Type.type_of("k") == Type.KING assert Type.type_of("K") == Type.KING - assert Type.type_of(".") == Type.EMPTY + assert Type.type_of("・") == Type.EMPTY assert Type.type_of("b") == None @@ -34,7 +34,7 @@ def test_symbol(): assert Type.symbol(Type.HORSE, Color.RED, Language.ZH) == "马" assert Type.symbol(Type.HORSE, Color.BLACK, Language.ZH) == "馬" - assert Type.symbol(Type.EMPTY, Color.RED) == "." - assert Type.symbol(Type.EMPTY, Color.BLACK) == "." - assert Type.symbol(Type.EMPTY, Color.RED, Language.ZH) == "." - assert Type.symbol(Type.EMPTY, Color.BLACK, Language.ZH) == "." + assert Type.symbol(Type.EMPTY, Color.RED) == "・" + assert Type.symbol(Type.EMPTY, Color.BLACK) == "・" + assert Type.symbol(Type.EMPTY, Color.RED, Language.ZH) == "・" + assert Type.symbol(Type.EMPTY, Color.BLACK, Language.ZH) == "・" diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 3a122ed9..3d848f10 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -15,97 +15,97 @@ import cirq from unitary import alpha from unitary.alpha.quantum_effect import QuantumEffect -from unitary.examples.quantum_chinese_chess.board import Board +from unitary.examples.quantum_chinese_chess.board import * from unitary.examples.quantum_chinese_chess.piece import Piece from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type # TODO(): now the class is no longer the base class of all chess moves. Maybe convert this class # to a helper class to save each move (with its pop results) in a string form into move history. -class Move(QuantumEffect): - """The base class of all chess moves.""" - - def __init__( - self, - source: str, - target: str, - board: Board, - source2: Optional[str] = None, - target2: Optional[str] = None, - move_type: Optional[MoveType] = None, - move_variant: Optional[MoveVariant] = None, - ): - self.source = source - self.source2 = source2 - self.target = target - self.target2 = target2 - self.move_type = move_type - self.move_variant = move_variant - self.board = board - - def __eq__(self, other): - if isinstance(other, Move): - return ( - self.source == other.source - and self.source2 == other.source2 - and self.target == other.target - and self.target2 == other.target2 - and self.move_type == other.move_type - and self.move_variant == other.move_variant - ) - return False - - def _verify_objects(self, *objects): - # TODO(): add checks that apply to all move types - return - - def effect(self, *objects): - # TODO(): add effects according to move_type and move_variant - return - - def is_split_move(self) -> bool: - return self.target2 is not None - - def is_merge_move(self) -> bool: - return self.source2 is not None - - def to_str(self, verbose_level: int = 1) -> str: - """Constructs the string representation of the move. - According to the value of verbose_level: - - 1: only returns the move source(s) and target(s); - - 2: additionally returns the move type and variant; - - 3: additionally returns the source(s) and target(s) piece type and color. - """ - if verbose_level < 1: - return "" - - if self.is_split_move(): - move_str = [self.source + "^" + self.target + str(self.target2)] - elif self.is_merge_move(): - move_str = [self.source + str(self.source2) + "^" + self.target] - else: - move_str = [self.source + self.target] - - if verbose_level > 1: - move_str.append(self.move_type.name) - move_str.append(self.move_variant.name) - - if verbose_level > 2: - source = self.board.board[self.source] - target = self.board.board[self.target] - move_str.append( - source.color.name - + "_" - + source.type_.name - + "->" - + target.color.name - + "_" - + target.type_.name - ) - return ":".join(move_str) - - def __str__(self): - return self.to_str() +# class Move(QuantumEffect): +# """The base class of all chess moves.""" + +# def __init__( +# self, +# source: str, +# target: str, +# board: Board, +# source2: Optional[str] = None, +# target2: Optional[str] = None, +# move_type: Optional[MoveType] = None, +# move_variant: Optional[MoveVariant] = None, +# ): +# self.source = source +# self.source2 = source2 +# self.target = target +# self.target2 = target2 +# self.move_type = move_type +# self.move_variant = move_variant +# self.board = board + +# def __eq__(self, other): +# if isinstance(other, Move): +# return ( +# self.source == other.source +# and self.source2 == other.source2 +# and self.target == other.target +# and self.target2 == other.target2 +# and self.move_type == other.move_type +# and self.move_variant == other.move_variant +# ) +# return False + +# def _verify_objects(self, *objects): +# # TODO(): add checks that apply to all move types +# return + +# def effect(self, *objects): +# # TODO(): add effects according to move_type and move_variant +# return + +# def is_split_move(self) -> bool: +# return self.target2 is not None + +# def is_merge_move(self) -> bool: +# return self.source2 is not None + +# def to_str(self, verbose_level: int = 1) -> str: +# """Constructs the string representation of the move. +# According to the value of verbose_level: +# - 1: only returns the move source(s) and target(s); +# - 2: additionally returns the move type and variant; +# - 3: additionally returns the source(s) and target(s) piece type and color. +# """ +# if verbose_level < 1: +# return "" + +# if self.is_split_move(): +# move_str = [self.source + "^" + self.target + str(self.target2)] +# elif self.is_merge_move(): +# move_str = [self.source + str(self.source2) + "^" + self.target] +# else: +# move_str = [self.source + self.target] + +# if verbose_level > 1: +# move_str.append(self.move_type.name) +# move_str.append(self.move_variant.name) + +# if verbose_level > 2: +# source = self.board.board[self.source] +# target = self.board.board[self.target] +# move_str.append( +# source.color.name +# + "_" +# + source.type_.name +# + "->" +# + target.color.name +# + "_" +# + target.type_.name +# ) +# return ":".join(move_str) + +# def __str__(self): +# return self.to_str() class Jump(QuantumEffect): @@ -528,13 +528,13 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: 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 not world.pop([source_0])[0].value: + 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 not world.pop([target_0])[0].value: + 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(()) @@ -551,7 +551,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: quantum_path_pieces_0[0].reset() could_capture = True else: - source_0.is_entangled = True # 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 @@ -561,13 +560,11 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.Flip() )(capture_ancilla) # We measure this ancilla to determine if the cannon fire could be made. - could_capture = world.pop([capture_ancilla])[0].value + could_capture = world.pop([capture_ancilla])[0] if not could_capture: # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player # could try to do this move again. Is this desired? - print( - "Cannon fire not applied: the source turns out to be empty or the path turns out to be blocked." - ) + print("Cannon fire not applied: tthe path turns out to be blocked.") return iter(()) # Apply the capture. # Quantumly reset the target. @@ -591,7 +588,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: # path piece(s), the cannon could fire and capture only if there is exactly # one quantum path piece being occupied. could_capture = False - source_0.is_entangled = True # TODO(): think a more efficient way of implementing this case. # 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. @@ -618,7 +614,7 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: alpha.Flip() )(capture_ancilla) # We measure the ancilla to determine if the fire could be made. - could_capture = world.pop([capture_ancilla])[0].value + could_capture = world.pop([capture_ancilla])[0] if could_capture: # Apply the capture. world.unhook(target_0) @@ -637,6 +633,6 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: return iter(()) # Reaching the end of the for loop means the fire could not be made. print( - "Cannon fire not applied: either the source turns out be empty, or there turns out to be (!=1) occupied path pieces." + "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 3e7b9a72..62302fa9 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -11,16 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from unitary.examples.quantum_chinese_chess.move import ( - Move, - Jump, - SplitJump, - MergeJump, - Slide, - SplitSlide, - MergeSlide, - CannonFire, -) +from unitary.examples.quantum_chinese_chess.move import * from unitary.examples.quantum_chinese_chess.board import Board from unitary.examples.quantum_chinese_chess.piece import Piece import pytest @@ -615,7 +606,7 @@ def test_move_type(): def test_split_slide(): - # Source is in classical state + one arm is 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"]) @@ -645,27 +636,311 @@ def test_split_slide(): board, { # both paths blocked - locations_to_bitboard(["a2", "b2", "c2", "d2"]): 1.0 / 17, - locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1.0 / 17, - locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1.0 / 17, - locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1.0 / 17, - locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1.0 / 17, - locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1.0 / 17, + 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 / 17, - locations_to_bitboard(["e1", "b3", "c3", "d2"]): 1.0 / 17, # slide to e1 + 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 / 17, # slide to e2 - locations_to_bitboard(["e2", "b3", "c2", "d3"]): 1.0 / 17, # slide to e2 - locations_to_bitboard(["e2", "b2", "c3", "d3"]): 1.0 / 17, # slide to e2 - locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1.0 / 17, - locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1.0 / 17, - locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1.0 / 17, + 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 / 17, # slide to e1 - locations_to_bitboard(["e2", "b3", "c3", "d3"]): 1.0 / 17, # slide to e2 - locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1.0 / 17, + 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, }, ) # 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 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, + }, + ) + + # 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 + }, + ) + + # 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(): + # 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}) + + # 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} + ) + + # 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} + ) + + # 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, + }, + ) diff --git a/unitary/examples/quantum_chinese_chess/piece_test.py b/unitary/examples/quantum_chinese_chess/piece_test.py index 6141ff0a..8508c170 100644 --- a/unitary/examples/quantum_chinese_chess/piece_test.py +++ b/unitary/examples/quantum_chinese_chess/piece_test.py @@ -34,9 +34,9 @@ def test_symbol(): assert p1.symbol(Language.ZH) == "馬" p2 = Piece("c2", SquareState.EMPTY, Type.EMPTY, Color.NA) - assert p2.symbol() == "." - assert p2.__str__() == "." - assert p2.symbol(Language.ZH) == "." + assert p2.symbol() == "・" + assert p2.__str__() == "・" + assert p2.symbol(Language.ZH) == "・" def test_enum(): From 5883cacf32583ec5dce8fbe5c556c22a42fb7d13 Mon Sep 17 00:00:00 2001 From: madcpf Date: Thu, 19 Oct 2023 21:48:51 -0700 Subject: [PATCH 13/31] update --- .../quantum_chinese_chess/board_test.py | 189 +++++++++ .../quantum_chinese_chess/chess_test.py | 358 ++++++++++++++++++ 2 files changed, 547 insertions(+) create mode 100644 unitary/examples/quantum_chinese_chess/board_test.py create mode 100644 unitary/examples/quantum_chinese_chess/chess_test.py diff --git a/unitary/examples/quantum_chinese_chess/board_test.py b/unitary/examples/quantum_chinese_chess/board_test.py new file mode 100644 index 00000000..b6cf37e7 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/board_test.py @@ -0,0 +1,189 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from unitary.examples.quantum_chinese_chess.enums import ( + Language, + Color, + Type, + SquareState, +) +from unitary.examples.quantum_chinese_chess.board import * +from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.test_utils import ( + locations_to_bitboard, + assert_samples_in, + assert_sample_distribution, + get_board_probability_distribution, + set_board, +) + + +# def test_init_with_default_fen(): +# board = Board.from_fen() +# assert ( +# board.__str__() +# == """ +# a b c d e f g h i +# 9 r h e a k a e h r 9 +# 8 . . . . . . . . . 8 +# 7 . c . . . . . c . 7 +# 6 p . p . p . p . p 6 +# 5 . . . . . . . . . 5 +# 4 . . . . . . . . . 4 +# 3 P . P . P . P . P 3 +# 2 . C . . . . . C . 2 +# 1 . . . . . . . . . 1 +# 0 R H E A K A E H R 0 +# a b c d e f g h i +# """ +# ) + +# board.set_language(Language.ZH) +# assert ( +# board.__str__() +# == """ +#  abcdefghi +# 9車馬相仕帥仕相馬車9 +# 8.........8 +# 7.砲.....砲.7 +# 6卒.卒.卒.卒.卒6 +# 5.........5 +# 4.........4 +# 3兵.兵.兵.兵.兵3 +# 2.炮.....炮.2 +# 1.........1 +# 0车马象士将士象马车0 +#  abcdefghi +# """ +# ) + +# assert board.king_locations == ["e0", "e9"] + + +# def test_init_with_specified_fen(): +# board = Board.from_fen("4kaR2/4a4/3hR4/7H1/9/9/9/9/4Ap1r1/3AK3c w---1 ") + +# assert ( +# board.__str__() +# == """ +# a b c d e f g h i +# 9 . . . A K . . . c 9 +# 8 . . . . A p . r . 8 +# 7 . . . . . . . . . 7 +# 6 . . . . . . . . . 6 +# 5 . . . . . . . . . 5 +# 4 . . . . . . . . . 4 +# 3 . . . . . . . H . 3 +# 2 . . . h R . . . . 2 +# 1 . . . . a . . . . 1 +# 0 . . . . k a R . . 0 +# a b c d e f g h i +# """ +# ) + +# board.set_language(Language.ZH) +# assert ( +# board.__str__() +# == """ +#  abcdefghi +# 9...士将...砲9 +# 8....士卒.車.8 +# 7.........7 +# 6.........6 +# 5.........5 +# 4.........4 +# 3.......马.3 +# 2...馬车....2 +# 1....仕....1 +# 0....帥仕车..0 +#  abcdefghi +# """ +# ) + +# assert board.king_locations == ["e0", "e9"] + + +def test_path_pieces(): + board = Board.from_fen() + # In case of only moving one step, return empty path pieces. + assert board.path_pieces("a0", "a1") == ([], []) + + # In case of advisor moving, return empty path pieces. + assert board.path_pieces("d0", "e1") == ([], []) + + # In case of elephant move, there should be at most one path piece. + assert board.path_pieces("c0", "e2") == ([], []) + # Add one classical piece in the path. + board.board["d1"].reset(Piece("d1", SquareState.OCCUPIED, Type.ROOK, Color.RED)) + assert board.path_pieces("c0", "e2") == (["d1"], []) + # Add one quantum piece in the path. + board.board["d1"].is_entangled = True + assert board.path_pieces("c0", "e2") == ([], ["d1"]) + + # Horizontal move + board.board["c7"].reset(Piece("c7", SquareState.OCCUPIED, Type.ROOK, Color.RED)) + board.board["c7"].is_entangled = True + assert board.path_pieces("a7", "i7") == (["b7", "h7"], ["c7"]) + + # Vertical move + assert board.path_pieces("c0", "c9") == (["c3", "c6"], ["c7"]) + + # In case of horse move, there should be at most one path piece. + assert board.path_pieces("b9", "a7") == ([], []) + assert board.path_pieces("b9", "c7") == ([], []) + # One classical piece in path. + assert board.path_pieces("b9", "d8") == (["c9"], []) + # One quantum piece in path. + assert board.path_pieces("c8", "d6") == ([], ["c7"]) + + +def test_flying_general_check(): + board = Board.from_fen() + # If they are in different columns, the check fails. + board.king_locations = ["d0", "e9"] + assert board.flying_general_check() == False + + # If there are classical pieces between two KINGs, the check fails. + board.king_locations = ["e0", "e9"] + assert board.flying_general_check() == False + + # If there are no pieces between two KINGs, the check successes. + board.board["e3"].reset() + board.board["e6"].reset() + assert board.flying_general_check() == True + + # When there are quantum pieces in between. + board = set_board(["a3", "a4", "e0", "e9"]) + board.king_locations = ["e0", "e9"] + board.current_player = 0 # i.e. RED + world = board.board + SplitJump()(world["a3"], world["c3"], world["e3"]) + SplitJump()(world["a4"], world["c4"], world["e4"]) + + result = board.flying_general_check() + # We check the ancilla to learn whether the general/king flies or not. + captured = world.post_selection[world["ancilla_ancilla_flying_general_check_0_0"]] + if captured: + assert result + assert_samples_in(board, {locations_to_bitboard(["c3", "c4", "e0"]): 1.0}) + else: + assert not result + assert_sample_distribution( + board, + { + locations_to_bitboard(["e0", "e9", "e3", "e4"]): 1.0 / 3, + locations_to_bitboard(["e0", "e9", "e3", "c4"]): 1.0 / 3, + locations_to_bitboard(["e0", "e9", "c3", "e4"]): 1.0 / 3, + }, + ) diff --git a/unitary/examples/quantum_chinese_chess/chess_test.py b/unitary/examples/quantum_chinese_chess/chess_test.py new file mode 100644 index 00000000..bd00ec65 --- /dev/null +++ b/unitary/examples/quantum_chinese_chess/chess_test.py @@ -0,0 +1,358 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +import io +import sys +from unitary.examples.quantum_chinese_chess.chess import QuantumChineseChess +from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.enums import ( + Language, + Color, + Type, + SquareState, + MoveType, + MoveVariant, +) + + +def test_game_init(monkeypatch): + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + output = io.StringIO() + sys.stdout = output + game = QuantumChineseChess() + assert game.lang == Language.ZH + assert game.players_name == ["Bob", "Ben"] + assert game.current_player == 0 + assert "Welcome" in output.getvalue() + sys.stdout = sys.__stdout__ + + +def test_parse_input_string_success(monkeypatch): + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + assert game.parse_input_string("a1b1") == (["a1"], ["b1"]) + assert game.parse_input_string("a1b1^c2") == (["a1", "b1"], ["c2"]) + assert game.parse_input_string("a1^b1c2") == (["a1"], ["b1", "c2"]) + + +def test_parse_input_string_fail(monkeypatch): + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + with pytest.raises(ValueError, match="Invalid sources/targets string "): + game.parse_input_string("a1^b1") + with pytest.raises(ValueError, match="Invalid sources/targets string "): + game.parse_input_string("a^1b1c2") + with pytest.raises(ValueError, match="Two sources should not be the same."): + game.parse_input_string("a1a1^c2") + with pytest.raises(ValueError, match="Two targets should not be the same."): + game.parse_input_string("a1^c2c2") + with pytest.raises(ValueError, match="Invalid sources/targets string "): + game.parse_input_string("a1b") + with pytest.raises(ValueError, match="Source and target should not be the same."): + game.parse_input_string("a1a1") + with pytest.raises(ValueError, match="Invalid location string."): + game.parse_input_string("a1n1") + + +def test_apply_move_fail(monkeypatch): + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + with pytest.raises(ValueError, match="Could not move empty piece."): + game.apply_move("a8b8") + with pytest.raises(ValueError, match="Could not move the other player's piece."): + game.apply_move("a9b8") + with pytest.raises(ValueError, match="Two sources need to be the same type."): + game.apply_move("a0a3^a4") + with pytest.raises(ValueError, match="Two targets need to be the same type."): + game.apply_move("b2^a2h2") + with pytest.raises(ValueError, match="Two targets need to be the same color."): + game.apply_move("b2^b7h2") + + +def test_game_invalid_move(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben", "a1n1", "exit"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + game.play() + assert ( + "Invalid location string. Make sure they are from a0 to i9." + in output.getvalue() + ) + sys.stdout = sys.__stdout__ + + +def test_check_classical_rule(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + # The move is blocked by classical path piece. + with pytest.raises(ValueError, match="The path is blocked."): + game.check_classical_rule("a0", "a4", ["a3"]) + + # Target should not be a classical piece of the same color. + with pytest.raises( + ValueError, match="The target place has classical piece with the same color." + ): + game.check_classical_rule("a0", "a3", []) + + # ROOK + game.check_classical_rule("a0", "a2", []) + with pytest.raises(ValueError, match="ROOK cannot move like this."): + game.check_classical_rule("a0", "b1", []) + + # HORSE + game.check_classical_rule("b0", "c2", []) + with pytest.raises(ValueError, match="HORSE cannot move like this."): + game.check_classical_rule("b0", "c1", []) + + # ELEPHANT + game.check_classical_rule("c0", "e2", []) + with pytest.raises(ValueError, match="ELEPHANT cannot move like this."): + game.check_classical_rule("c0", "e1", []) + game.board.board["g5"].reset( + Piece("g5", SquareState.OCCUPIED, Type.ELEPHANT, Color.BLACK) + ) + with pytest.raises(ValueError, match="ELEPHANT cannot cross the river"): + game.check_classical_rule("g5", "i3", []) + game.board.board["c4"].reset( + Piece("c4", SquareState.OCCUPIED, Type.ELEPHANT, Color.RED) + ) + with pytest.raises(ValueError, match="ELEPHANT cannot cross the river"): + game.check_classical_rule("c4", "e6", []) + + # ADVISOR + game.check_classical_rule("d9", "e8", []) + with pytest.raises(ValueError, match="ADVISOR cannot move like this."): + game.check_classical_rule("d9", "d8", []) + with pytest.raises(ValueError, match="ADVISOR cannot leave the palace."): + game.check_classical_rule("d9", "c8", []) + with pytest.raises(ValueError, match="ADVISOR cannot leave the palace."): + game.check_classical_rule("f0", "g1", []) + + # KING + game.check_classical_rule("e9", "e8", []) + with pytest.raises(ValueError, match="KING cannot move like this."): + game.check_classical_rule("e9", "d8", []) + game.board.board["c0"].reset() + game.board.board["d0"].reset(game.board.board["e0"]) + game.board.board["e0"].reset() + with pytest.raises(ValueError, match="KING cannot leave the palace."): + game.check_classical_rule("d0", "c0", []) + + # CANNON + game.check_classical_rule("b7", "b4", []) + with pytest.raises(ValueError, match="CANNON cannot move like this."): + game.check_classical_rule("b7", "a8", []) + # Cannon could jump across exactly one piece. + game.check_classical_rule("b2", "b9", ["b7"]) + with pytest.raises(ValueError, match="CANNON cannot fire like this."): + game.check_classical_rule("b2", "b9", ["b5", "b7"]) + # Cannon cannot fire to a piece with same color. + game.board.board["b3"].reset(game.board.board["b2"]) + game.board.board["b2"].reset() + game.board.board["e3"].is_entangled = True + with pytest.raises( + ValueError, match="CANNON cannot fire to a piece with same color." + ): + game.check_classical_rule("b3", "e3", ["c3"]) + with pytest.raises(ValueError, match="CANNON cannot fire to an empty piece."): + game.check_classical_rule("b3", "d3", ["c3"]) + + # PAWN + game.check_classical_rule("a3", "a4", []) + with pytest.raises(ValueError, match="PAWN cannot move like this."): + game.check_classical_rule("a3", "a5", []) + with pytest.raises( + ValueError, match="PAWN can only go forward before crossing the river" + ): + game.check_classical_rule("e3", "f3", []) + with pytest.raises( + ValueError, match="PAWN can only go forward before crossing the river" + ): + game.check_classical_rule("g6", "h6", []) + with pytest.raises(ValueError, match="PAWN can not move backward."): + game.check_classical_rule("a3", "a2", []) + with pytest.raises(ValueError, match="PAWN can not move backward."): + game.check_classical_rule("g6", "g7", []) + # After crossing the rive the pawn could move horizontally. + game.board.board["c4"].reset(game.board.board["c6"]) + game.board.board["c6"].reset() + game.check_classical_rule("c4", "b4", []) + game.check_classical_rule("c4", "d4", []) + + +def test_classify_move_fail(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + with pytest.raises( + ValueError, match="CANNON could not fire/capture without a cannon platform." + ): + game.classify_move(["b7"], ["b2"], [], [], [], []) + + with pytest.raises( + ValueError, match="Both sources need to be in quantum state in order to merge." + ): + game.classify_move(["b2", "h2"], ["e2"], [], [], [], []) + + game.board.board["c0"].reset(game.board.board["b7"]) + game.board.board["c0"].is_entangled = True + game.board.board["b7"].reset() + game.board.board["g0"].reset(game.board.board["h7"]) + game.board.board["g0"].is_entangled = True + game.board.board["h7"].reset() + with pytest.raises(ValueError, match="Currently CANNON cannot merge while firing."): + game.classify_move(["c0", "g0"], ["e0"], ["d0"], [], ["f0"], []) + + game.board.board["b3"].reset(game.board.board["b2"]) + game.board.board["b3"].is_entangled = True + game.board.board["b2"].reset() + game.board.board["d3"].reset(game.board.board["h2"]) + game.board.board["d3"].is_entangled = True + game.board.board["h2"].reset() + with pytest.raises( + ValueError, match="Currently we could only merge into an empty piece." + ): + game.classify_move(["b3", "d3"], ["c3"], [], [], [], []) + + with pytest.raises(ValueError, match="Currently CANNON cannot split while firing."): + game.classify_move(["g0"], ["e0", "i0"], ["f0"], [], ["h0"], []) + + game.board.board["d0"].is_entangled = True + with pytest.raises( + ValueError, match="Currently we could only split into empty pieces." + ): + game.classify_move(["d3"], ["d0", "d4"], [], [], [], []) + + game.board.board["d0"].reset() + game.board.board["f0"].reset() + with pytest.raises(ValueError, match="King split is not supported currently."): + game.classify_move(["e0"], ["d0", "f0"], [], [], [], []) + + +def test_classify_move_success(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + # classical + assert game.classify_move(["h9"], ["g7"], [], [], [], []) == ( + MoveType.CLASSICAL, + MoveVariant.CLASSICAL, + ) + assert game.classify_move(["b2"], ["b9"], ["b7"], [], [], []) == ( + MoveType.CLASSICAL, + MoveVariant.CLASSICAL, + ) + + # jump basic + game.board.board["c9"].is_entangled = True + assert game.classify_move(["c9"], ["e7"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.BASIC, + ) + game.board.board["b2"].is_entangled = True + assert game.classify_move(["b2"], ["e2"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.BASIC, + ) + + # jump excluded + game.board.board["a3"].is_entangled = True + assert game.classify_move(["a0"], ["a3"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.EXCLUDED, + ) + + # jump capture + game.board.board["g4"].reset(game.board.board["g6"]) + game.board.board["g4"].is_entangled = True + game.board.board["g6"].reset() + assert game.classify_move(["g4"], ["g3"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.CAPTURE, + ) + + # slide basic + assert game.classify_move(["a0"], ["a4"], [], ["a3"], [], []) == ( + MoveType.SLIDE, + MoveVariant.BASIC, + ) + + # slide excluded + game.board.board["i7"].reset(game.board.board["h7"]) + game.board.board["i7"].is_entangled = True + game.board.board["h7"].reset() + game.board.board["i6"].is_entangled = True + assert game.classify_move(["i9"], ["i6"], [], ["i7"], [], []) == ( + MoveType.SLIDE, + MoveVariant.EXCLUDED, + ) + + # slide capture + assert game.classify_move(["a0"], ["a6"], [], ["a3"], [], []) == ( + MoveType.SLIDE, + MoveVariant.CAPTURE, + ) + + # split_jump basic + assert game.classify_move(["g4"], ["f4", "h4"], [], [], [], []) == ( + MoveType.SPLIT_JUMP, + MoveVariant.BASIC, + ) + + # split_slide basic + game.board.board["d3"].reset(game.board.board["h2"]) + game.board.board["h2"].reset() + game.board.board["c3"].is_entangled = True + game.board.board["e3"].is_entangled = True + assert game.classify_move(["d3"], ["b3", "f3"], [], ["c3"], [], ["e3"]) == ( + MoveType.SPLIT_SLIDE, + MoveVariant.BASIC, + ) + + # merge_jump basic + game.board.board["b7"].is_entangled = True + assert game.classify_move(["b7", "i7"], ["e7"], [], [], [], []) == ( + MoveType.MERGE_JUMP, + MoveVariant.BASIC, + ) + + # merge_slide basic + assert game.classify_move(["b7", "i7"], ["a7"], [], [], [], ["b7"]) == ( + MoveType.MERGE_SLIDE, + MoveVariant.BASIC, + ) + + # cannon_fire capture + assert game.classify_move(["i7"], ["i3"], [], ["i6"], [], []) == ( + MoveType.CANNON_FIRE, + MoveVariant.CAPTURE, + ) + game.board.board["i6"].is_entangled = False + assert game.classify_move(["i7"], ["i3"], ["i6"], [], [], []) == ( + MoveType.CANNON_FIRE, + MoveVariant.CAPTURE, + ) From 7f7af9fc6e118baac0731a5f224cef8fd7c84358 Mon Sep 17 00:00:00 2001 From: madcpf Date: Fri, 20 Oct 2023 11:42:11 -0700 Subject: [PATCH 14/31] test --- unitary/alpha/quantum_effect.py | 7 +- unitary/alpha/quantum_world.py | 36 +++- unitary/alpha/qubit_effects.py | 2 + .../examples/quantum_chinese_chess/board.py | 44 ++-- .../examples/quantum_chinese_chess/chess.py | 84 ++++++-- .../examples/quantum_chinese_chess/enums.py | 2 +- .../quantum_chinese_chess/move_test.py | 204 ++++++++++++------ 7 files changed, 272 insertions(+), 107 deletions(-) diff --git a/unitary/alpha/quantum_effect.py b/unitary/alpha/quantum_effect.py index f60623ba..ee8779d1 100644 --- a/unitary/alpha/quantum_effect.py +++ b/unitary/alpha/quantum_effect.py @@ -60,7 +60,12 @@ def __call__(self, *objects): """Apply the Quantum Effect to the objects.""" self._verify_objects(*objects) world = objects[0].world - world.add_effect(list(self.effect(*objects))) + effects = list(self.effect(*objects)) + if len(effects) > 0: + print("### call of QuantumEffect") + print(effects) + world.add_effect(effects) + print("\n\n") def __str__(self): return self.__class__.__name__ diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index fe777fa7..cee5f044 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -248,11 +248,17 @@ def _compile_op(self, op: cirq.Operation) -> Union[cirq.Operation, cirq.OP_TREE] def add_effect(self, op_list: List[cirq.Operation]): """Adds an operation to the current circuit.""" + print("## Add_effect") + print(len(self.effect_history)) self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection)) ) for op in op_list: + print("### op") + print(op) self._append_op(op) + print("## Add_effect 2") + print(len(self.effect_history)) def undo_last_effect(self): """Restores the `QuantumWorld` to the state before the last effect. @@ -266,6 +272,8 @@ def undo_last_effect(self): if not self.effect_history: raise IndexError("No effects to undo") self.circuit, self.post_selection = self.effect_history.pop() + print("## undo_last_effect") + print(len(self.effect_history)) def _suggest_num_reps(self, sample_size: int) -> int: """Guess the number of raw samples needed to get sample_size results. @@ -305,6 +313,20 @@ def _interpret_result(self, result: Union[int, Iterable[int]]) -> int: return result_list[0] return result + def unhook(self, object: QuantumObject) -> None: + """Replaces all usages of the given object in the circuit with a new ancilla with value=0.""" + # Creates a new ancilla. + new_ancilla = self._add_ancilla(object.name) + # Replace operations using the qubit of the given object with the new ancilla. + qubit_remapping_dict = { + object.qubit: new_ancilla.qubit, + new_ancilla.qubit: object.qubit, + } + self.circuit = self.circuit.transform_qubits( + lambda q: qubit_remapping_dict.get(q, q) + ) + return + def force_measurement( self, obj: QuantumObject, result: Union[enum.Enum, int] ) -> None: @@ -316,7 +338,6 @@ def force_measurement( state of the result. """ new_obj = self._add_ancilla(namespace=obj.name, value=result) - print(new_obj.name) # 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: @@ -424,6 +445,8 @@ def pop( objects: Optional[Sequence[Union[QuantumObject, str]]] = None, convert_to_enum: bool = True, ) -> List[Union[enum.Enum, int]]: + print("## pop") + print(len(self.effect_history)) self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection)) ) @@ -437,6 +460,8 @@ def pop( results = self.peek(quantum_objects, convert_to_enum=convert_to_enum) for idx, result in enumerate(results[0]): self.force_measurement(quantum_objects[idx], result) + print("## pop 2") + print(len(self.effect_history)) return results[0] @@ -515,12 +540,3 @@ def __getitem__(self, name: str) -> QuantumObject: for obj in self.object_name_dict.keys(): print(obj) raise KeyError(f"{name} did not exist in this world.") - - def unhook(self, object: QuantumObject) -> None: - new_ancilla = self._add_ancilla(object.name) - # Replace operations using the qubit of the given object with the ancilla instead - qubit_remapping_dict = {object.qubit: new_ancilla.qubit, new_ancilla.qubit: object.qubit} - self.circuit = self.circuit.transform_qubits( - lambda q: qubit_remapping_dict.get(q, q) - ) - return diff --git a/unitary/alpha/qubit_effects.py b/unitary/alpha/qubit_effects.py index 9a659a18..8f0b7810 100644 --- a/unitary/alpha/qubit_effects.py +++ b/unitary/alpha/qubit_effects.py @@ -207,9 +207,11 @@ def num_objects(self): return 3 def effect(self, *objects): + print("## PhasedSplit") yield cirq.ISWAP(objects[0].qubit, objects[1].qubit) ** 0.5 yield cirq.ISWAP(objects[0].qubit, objects[2].qubit) ** 0.5 yield cirq.ISWAP(objects[0].qubit, objects[2].qubit) ** 0.5 + print("## PhasedSplit 1") def __eq__(self, other): return isinstance(other, PhasedSplit) or NotImplemented diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index ed1ab2e4..b47d2f59 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -30,7 +30,9 @@ negative = "\033[03m" underline = "\033[04m" blink = "\033[05m" +reverse = "\033[07m" invisible = "\033[08m" +strikethrough = "\033[09m" # background grey = "\033[47m" @@ -38,6 +40,11 @@ # foreground black = "\033[30m" red = "\033[31m" +green = "\033[32m" +lightred = "\033[91m" +lightgreen = "\033[92m" +lightgrey = "\033[37m" +darkgrey = "\033[90m" # The default initial state of the game. _INITIAL_FEN = "RHEAKAEHR/9/1C5C1/P1P1P1P1P/9/9/p1p1p1p1p/1c5c1/9/rheakaehr w---1" @@ -104,12 +111,14 @@ def to_str(self, probs: List[float] = None): if probs is None: probs = self.board.get_binary_probabilities() # Print the top line of col letters. + board_string += grey + board_string += black for col in "abcdefghi": if self.lang == Language.EN: board_string.append(f" {col} ") else: board_string.append(f"{col} ") - board_string.append("\n") + board_string += "\b" + reset + " \n" for row in range(num_rows): # Print the row index on the left. if self.lang == Language.EN: @@ -118,13 +127,19 @@ def to_str(self, probs: List[float] = None): board_string.append(f"{row} ") for col in "abcdefghi": piece = self.board[f"{col}{row}"] - board_string += grey + # board_string += grey if piece.is_entangled: - board_string += dim - if piece.color == Color.RED: - board_string += red + if piece.color == Color.RED: + board_string += lightred + else: + board_string += lightgrey else: - board_string += black + # board_string += bold + if piece.color == Color.RED: + board_string += red + else: + # board_string += black + pass board_string += piece.symbol(self.lang) if col != "i": if self.lang == Language.EN: @@ -133,28 +148,29 @@ def to_str(self, probs: List[float] = None): board_string.append(" ") board_string += reset # Print the row index on the right. - board_string.append(f" {row}\n ") + board_string.append(f" {row}\n ") # Print the sampled prob. of the pieces in the above row. - # We need to do the following range() conversion since the sequence of - # qubits returned from get_binary_probabilities() is - # a9 b9 ... i9, a8 b8 ... i8, ..., a0 b0 ... i0 - for i in range((num_rows - row - 1) * 9, (num_rows - row) * 9): + board_string += grey + board_string += black + for i in range(row * 9, (row + 1) * 9): board_string.append("{:.1f} ".format(probs[i])) - board_string.append("\n") + board_string += "\b" + reset + " \n" board_string.append(" ") # Print the bottom line of col letters. + board_string += grey + board_string += black for col in "abcdefghi": if self.lang == Language.EN: board_string.append(f" {col} ") else: board_string.append(f"{col} ") - board_string.append("\n") + board_string += "\b" + reset + " \n" if self.lang == Language.EN: return "".join(board_string) # We need to turn letters into their full-width counterparts to align # a mix of letters + Chinese characters. half_width_chars = "".join( - [" ", "・"] + [" ", "."] + [chr(c) for c in range(ord("A"), ord("Z"))] + [chr(c) for c in range(ord("a"), ord("z"))] ) diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 17c0a156..5d597deb 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -77,6 +77,12 @@ def __init__(self): self.game_state = GameState.CONTINUES self.current_player = self.board.current_player self.debug_level = 3 + # This variable is used to save the length of current effect history before each move is made, + # so that if we later undo we know how many effects we need to pop out. + self.effect_history_length = [] + # This variable is used to save the classical properties of the whole board before each move is + # made, so that if we later undo we could recover the earlier classical state. + self.classical_properties_history = [] @staticmethod def parse_input_string(str_to_parse: str) -> Tuple[List[str], List[str]]: @@ -401,17 +407,11 @@ def next_move(self) -> Tuple[bool, str]: # TODO(): make it look like the normal board. Right now it's only for debugging purposes. print(self.board.board.peek(convert_to_enum=False)) elif input_str.lower() == "undo": - output = "Undo last quantum effect." - # Right now it's only for debugging purposes, since it has following problems: - # TODO(): there are several problems here: - # 1) last move is quantum but classical piece information is not reversed back. - # ==> we may need to save the change of classical piece information of each step. - # 2) last move might not be quantum. - # ==> we may need to save all classical moves and figure out how to undo each kind of move; - # 3) last move is quantum but involved multiple effects. - # ==> we may need to save number of effects per move, and undo that number of times. - self.board.board.undo_last_effect() - return True, output + if len(self.effect_history_length) <= 1: + # length == 1 corresponds to the initial state, and no more undo could be made. + return False, "Unable to undo any more." + self.undo() + return True, "Undoing." else: try: # The move is success if no ValueError is raised. @@ -425,13 +425,10 @@ def update_board_by_sampling(self) -> List[float]: probs = self.board.board.get_binary_probabilities() num_rows = 10 num_cols = 9 - for row in range(num_rows): + for row in range(num_rows - 1, -1, -1): for col in "abcdefghi": piece = self.board.board[f"{col}{row}"] - # We need to do the following range() conversion since the sequence of - # qubits returned from get_binary_probabilities() is - # a9 b9 ... i9, a8 b8 ... i8, ..., a0 b0 ... i0 - prob = probs[(num_rows - row - 1) * num_cols + ord(col) - ord("a")] + prob = probs[row * num_cols + ord(col) - ord("a")] # TODO(): set more accurate threshold if prob < 1e-3: piece.reset() @@ -449,8 +446,52 @@ def game_over(self) -> None: # TODO(): add the following checks # - If player 0 made N repeatd back-and_forth moves in a row. + def save_snapshot(self) -> None: + """Saves the current length of the effect history, and the current classical states of all pieces.""" + # Save the current length of the effect history. + self.effect_history_length.append(len(self.board.board.effect_history)) + # Save the classical states of all pieces. + snapshot = [] + for row in range(10): + for col in "abcdefghi": + piece = self.board.board[f"{col}{row}"] + snapshot.append( + [piece.type_.value, piece.color.value, piece.is_entangled] + ) + self.classical_properties_history.append(snapshot) + + def undo(self) -> None: + """Undo the last move, which includes reset quantum effects and classical properties.""" + if ( + len(self.effect_history_length) <= 1 + or len(self.classical_properties_history) <= 1 + ): + # length == 1 corresponds to the initial state, and no more undo could be made. + raise ValueError("Unable to undo any more.") + + # Recover the effects up to the last snapshot (which was done after the last move finished) by + # popping effects out of the effect history of the board until its length equals the last + # snapshot's length. + self.effect_history_length.pop() + last_length = self.effect_history_length[-1] + while len(self.board.board.effect_history) > last_length: + self.board.board.undo_last_effect() + + # Recover the classical properties of all pieces to the last snapshot. + self.classical_properties_history.pop() + snapshot = self.classical_properties_history[-1] + index = 0 + for row in range(10): + for col in "abcdefghi": + piece = self.board.board[f"{col}{row}"] + piece.type_ = Type(snapshot[index][0]) + piece.color = Color(snapshot[index][1]) + piece.is_entangled = snapshot[index][2] + index += 1 + def play(self) -> None: """The loop where each player takes turn to play.""" + self.save_snapshot() while True: move_success, output = self.next_move() if not move_success: @@ -460,10 +501,13 @@ def play(self) -> None: print("\nPlease re-enter your move.") continue print(output) - # Check if the game is over. - # TODO(): maybe we should not check game_over() when an undo is made. - self.game_over() - probs = self.update_board_by_sampling() + if output != "Undoing.": + # Check if the game is over. + self.game_over() + # Update any empty or occupied pieces' classical state. + probs = self.update_board_by_sampling() + # Save the classical states and the current length of effect history. + self.save_snapshot() print(self.board.to_str(probs)) if self.game_state == GameState.CONTINUES: # If the game continues, switch the player. diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index 6341a409..0ebffeb2 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -85,7 +85,7 @@ class Type(enum.Enum): - Chinese black """ - EMPTY = ("・", "・", "・", "・") + EMPTY = (".", ".", ".", ".") PAWN = ("P", "p", "兵", "卒") CANNON = ("C", "c", "炮", "砲") ROOK = ("R", "r", "车", "車") diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 62302fa9..a8bbcd24 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -36,73 +36,73 @@ print_samples, set_board, ) +from unitary.examples.quantum_chinese_chess.chess import * +# def test_move_eq(): +# board = Board.from_fen() +# move1 = Move( +# "a1", +# "b2", +# board, +# "c1", +# move_type=MoveType.MERGE_JUMP, +# move_variant=MoveVariant.CAPTURE, +# ) +# move2 = Move( +# "a1", +# "b2", +# board, +# "c1", +# move_type=MoveType.MERGE_JUMP, +# move_variant=MoveVariant.CAPTURE, +# ) +# move3 = Move( +# "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE +# ) +# move4 = Move( +# "a1", +# "b2", +# board, +# "c1", +# move_type=MoveType.MERGE_SLIDE, +# move_variant=MoveVariant.CAPTURE, +# ) -def test_move_eq(): - board = Board.from_fen() - move1 = Move( - "a1", - "b2", - board, - "c1", - move_type=MoveType.MERGE_JUMP, - move_variant=MoveVariant.CAPTURE, - ) - move2 = Move( - "a1", - "b2", - board, - "c1", - move_type=MoveType.MERGE_JUMP, - move_variant=MoveVariant.CAPTURE, - ) - move3 = Move( - "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE - ) - move4 = Move( - "a1", - "b2", - board, - "c1", - move_type=MoveType.MERGE_SLIDE, - move_variant=MoveVariant.CAPTURE, - ) - - assert move1 == move2 - assert move1 != move3 - assert move1 != move4 +# assert move1 == move2 +# assert move1 != move3 +# assert move1 != move4 -def test_move_type(): - # TODO(): change to real senarios - board = Board.from_fen() - move1 = Move( - "a1", - "b2", - board, - "c1", - move_type=MoveType.MERGE_JUMP, - move_variant=MoveVariant.CAPTURE, - ) - assert move1.is_split_move() == False - assert move1.is_merge_move() +# def test_move_type(): +# # TODO(): change to real senarios +# board = Board.from_fen() +# move1 = Move( +# "a1", +# "b2", +# board, +# "c1", +# move_type=MoveType.MERGE_JUMP, +# move_variant=MoveVariant.CAPTURE, +# ) +# assert move1.is_split_move() == False +# assert move1.is_merge_move() - move2 = Move( - "a1", - "b2", - board, - target2="c1", - move_type=MoveType.SPLIT_JUMP, - move_variant=MoveVariant.BASIC, - ) - assert move2.is_split_move() - assert move2.is_merge_move() == False +# move2 = Move( +# "a1", +# "b2", +# board, +# target2="c1", +# move_type=MoveType.SPLIT_JUMP, +# move_variant=MoveVariant.BASIC, +# ) +# assert move2.is_split_move() +# assert move2.is_merge_move() == False - move3 = Move( - "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE - ) - assert move3.is_split_move() == False - assert move3.is_merge_move() == False +# move3 = Move( +# "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE +# ) +# assert move3.is_split_move() == False +# assert move3.is_merge_move() == False # def test_to_str(): @@ -944,3 +944,85 @@ def test_cannon_fire(): locations_to_bitboard(["a2", "b3", "c3", "d2"]): 1.0 / 2, }, ) + + +# TEST +def test_undo(monkeypatch): + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + board = set_board(["a1", "b1", "c1", "d1"]) + game.board = board + world = board.board + print("## Jump 1") + SplitJump()(world["a1"], world["a2"], world["a3"]) + print("## Jump 2") + SplitJump()(world["b1"], world["b2"], world["b3"]) + print("## Jump 3") + SplitJump()(world["c1"], world["c2"], world["c3"]) + print("## Jump 4") + SplitJump()(world["d1"], world["d2"], world["d3"]) + game.save_snapshot() + + print("## Fire ") + CannonFire([], ["b2", "c2"])(world["a2"], world["d2"]) + game.save_snapshot() + + # 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, + }, + ) + game.undo() + print("After undo.") + print(len(world.effect_history)) + assert 1 == 0 From b8140d565fbd9251c18d08a083c0e76ea9c638b3 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 09:42:46 -0700 Subject: [PATCH 15/31] update --- unitary/alpha/quantum_world.py | 24 ++++++++++++ .../examples/quantum_chinese_chess/board.py | 38 +++++++++++++------ .../examples/quantum_chinese_chess/chess.py | 14 ++++++- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index cee5f044..365c3532 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -489,6 +489,30 @@ def get_histogram( histogram[idx][cast(int, result[idx])] += 1 return histogram + def get_histogram_with_whole_world_as_key(self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 + ) -> Dict[Tuple[int], int]: + """Creates histogram based on measurements (peeks) carried out. + + Parameters: + objects: List of QuantumObjects + count: Number of measurements + + Returns: + A dictionary, with the keys being each possible state of the whole quantum world + (or `objects` if specified), and the values being the count of that state. + """ + if not objects: + objects = self.public_objects + peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) + histogram = {} + for result in peek_results: + key = tuple(result) + if key not in histogram: + histogram[key] = 1 + else: + histogram[key] += 1 + return histogram + def get_probabilities( self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 ) -> List[Dict[int, float]]: diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index b47d2f59..c3d9525f 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -104,12 +104,17 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": # TODO(): maybe add check to make sure the input fen itself is correct. return cls(board, current_player, king_locations) - def to_str(self, probs: List[float] = None): + def to_str( + self, + probabilities: List[float] = None, + print_probabilities=True, + peek_result: List[int] = None, + ): # TODO(): print players' names in their corresponding side of the board. num_rows = 10 board_string = ["\n "] - if probs is None: - probs = self.board.get_binary_probabilities() + if print_probabilities and probabilities is None: + probabilities = self.board.get_binary_probabilities() # Print the top line of col letters. board_string += grey board_string += black @@ -119,6 +124,7 @@ def to_str(self, probs: List[float] = None): else: board_string.append(f"{col} ") board_string += "\b" + reset + " \n" + index = 0 for row in range(num_rows): # Print the row index on the left. if self.lang == Language.EN: @@ -128,7 +134,7 @@ def to_str(self, probs: List[float] = None): for col in "abcdefghi": piece = self.board[f"{col}{row}"] # board_string += grey - if piece.is_entangled: + if peek_result is None and piece.is_entangled: if piece.color == Color.RED: board_string += lightred else: @@ -140,21 +146,31 @@ def to_str(self, probs: List[float] = None): else: # board_string += black pass - board_string += piece.symbol(self.lang) + if ( + peek_result is None + or piece.type_ == Type.EMPTY + or peek_result[index] == 1 + ): + board_string += piece.symbol(self.lang) + elif piece.is_entangled and peek_result[index] == 0: + board_string += Type.symbol(Type.EMPTY, Color.NA, self.lang) if col != "i": if self.lang == Language.EN: board_string.append(" ") else: board_string.append(" ") board_string += reset + index += 1 # Print the row index on the right. - board_string.append(f" {row}\n ") + board_string.append(f" {row}\n") # Print the sampled prob. of the pieces in the above row. - board_string += grey - board_string += black - for i in range(row * 9, (row + 1) * 9): - board_string.append("{:.1f} ".format(probs[i])) - board_string += "\b" + reset + " \n" + if print_probabilities: + board_string += " " + board_string += grey + board_string += black + for i in range(row * 9, (row + 1) * 9): + board_string.append("{:.1f} ".format(probabilities[i])) + board_string += "\b" + reset + " \n" board_string.append(" ") # Print the bottom line of col letters. board_string += grey diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index 5d597deb..d04724a8 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -405,7 +405,19 @@ def next_move(self) -> Tuple[bool, str]: output = "Exiting." elif input_str.lower() == "peek": # TODO(): make it look like the normal board. Right now it's only for debugging purposes. - print(self.board.board.peek(convert_to_enum=False)) + print( + self.board.to_str( + None, False, self.board.board.peek(convert_to_enum=False)[0] + ) + ) + elif input_str.lower() == "peek all": + all_boards = self.board.board.get_histogram_with_whole_world_as_key() + sorted_boards = sorted(all_boards.items(), key=lambda x: x[1], reverse=True) + for board, count in sorted_boards: + print( + "\n ====== With probability ~ {:.1f} ======".format(count / 100.0) + ) + print(self.board.to_str(None, False, list(board))) elif input_str.lower() == "undo": if len(self.effect_history_length) <= 1: # length == 1 corresponds to the initial state, and no more undo could be made. From 8731145eecc4c0638886c722cf27cbe175a239d6 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 10:08:06 -0700 Subject: [PATCH 16/31] update --- .../examples/quantum_chinese_chess/move.py | 626 ++-------- .../quantum_chinese_chess/move_test.py | 1096 +++-------------- 2 files changed, 232 insertions(+), 1490 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 3d848f10..a5cc7019 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -22,160 +22,90 @@ # TODO(): now the class is no longer the base class of all chess moves. Maybe convert this class # to a helper class to save each move (with its pop results) in a string form into move history. -# class Move(QuantumEffect): -# """The base class of all chess moves.""" - -# def __init__( -# self, -# source: str, -# target: str, -# board: Board, -# source2: Optional[str] = None, -# target2: Optional[str] = None, -# move_type: Optional[MoveType] = None, -# move_variant: Optional[MoveVariant] = None, -# ): -# self.source = source -# self.source2 = source2 -# self.target = target -# self.target2 = target2 -# self.move_type = move_type -# self.move_variant = move_variant -# self.board = board - -# def __eq__(self, other): -# if isinstance(other, Move): -# return ( -# self.source == other.source -# and self.source2 == other.source2 -# and self.target == other.target -# and self.target2 == other.target2 -# and self.move_type == other.move_type -# and self.move_variant == other.move_variant -# ) -# return False - -# def _verify_objects(self, *objects): -# # TODO(): add checks that apply to all move types -# return - -# def effect(self, *objects): -# # TODO(): add effects according to move_type and move_variant -# return - -# def is_split_move(self) -> bool: -# return self.target2 is not None - -# def is_merge_move(self) -> bool: -# return self.source2 is not None - -# def to_str(self, verbose_level: int = 1) -> str: -# """Constructs the string representation of the move. -# According to the value of verbose_level: -# - 1: only returns the move source(s) and target(s); -# - 2: additionally returns the move type and variant; -# - 3: additionally returns the source(s) and target(s) piece type and color. -# """ -# if verbose_level < 1: -# return "" - -# if self.is_split_move(): -# move_str = [self.source + "^" + self.target + str(self.target2)] -# elif self.is_merge_move(): -# move_str = [self.source + str(self.source2) + "^" + self.target] -# else: -# move_str = [self.source + self.target] - -# if verbose_level > 1: -# move_str.append(self.move_type.name) -# move_str.append(self.move_variant.name) - -# if verbose_level > 2: -# source = self.board.board[self.source] -# target = self.board.board[self.target] -# move_str.append( -# source.color.name -# + "_" -# + source.type_.name -# + "->" -# + target.color.name -# + "_" -# + target.type_.name -# ) -# return ":".join(move_str) - -# def __str__(self): -# return self.to_str() - - -class Jump(QuantumEffect): - """Jump from source_0 to target_0. The accepted move_variant includes - - CLASSICAL (where all classical moves will be handled here) - - CAPTURE - - EXCLUDED - - BASIC - """ +class Move(QuantumEffect): + """The base class of all chess moves.""" def __init__( self, - move_variant: MoveVariant, + source: str, + target: str, + board: Board, + source2: Optional[str] = None, + target2: Optional[str] = None, + move_type: Optional[MoveType] = None, + move_variant: Optional[MoveVariant] = None, ): + self.source = source + self.source2 = source2 + self.target = target + self.target2 = target2 + self.move_type = move_type self.move_variant = move_variant + self.board = board + + def __eq__(self, other): + if isinstance(other, Move): + return ( + self.source == other.source + and self.source2 == other.source2 + and self.target == other.target + and self.target2 == other.target2 + and self.move_type == other.move_type + and self.move_variant == other.move_variant + ) + return False - def num_dimension(self) -> Optional[int]: - return 2 + def _verify_objects(self, *objects): + # TODO(): add checks that apply to all move types + return - def num_objects(self) -> Optional[int]: - return 2 + def effect(self, *objects): + # TODO(): add effects according to move_type and move_variant + return - def effect(self, *objects) -> Iterator[cirq.Operation]: - # TODO(): currently pawn capture is the same as jump capture, while in quantum chess it's different, - # i.e. pawn would move only if the target is there, i.e. CNOT(t, s), and an entanglement could be - # created. This could be a general game setting, i.e. we could allow players to choose if they - # want the source piece to move (in case of capture) if the target piece is not there. - source_0, target_0 = objects - world = source_0.world - if self.move_variant == MoveVariant.CAPTURE: - # We peek and force measure source_0. - source_is_occupied = world.pop([source_0])[0].value - # For move_variant==CAPTURE, we require source_0 to be occupied before further actions. - # This is to prevent a piece of the board containing two types of different pieces. - if not source_is_occupied: - # If source_0 turns out to be not there, we set it to be EMPTY, and the jump - # could not be made. - source_0.reset() - print("Jump move not applied: source turns out to be empty.") - return iter(()) - source_0.is_entangled = False - # We replace the qubit of target_0 with a new ancilla, and set its classical properties to be EMPTY. - world.unhook(target_0) - target_0.reset() - elif self.move_variant == MoveVariant.EXCLUDED: - # We peek and force measure target_0. - target_is_occupied = world.pop([target_0])[0].value - # For move_variant==EXCLUDED, we require target_0 to be empty before further actions. - # This is to prevent a piece of the board containing two types of different pieces. - if target_is_occupied: - # If target_0 turns out to be there, we set it to be classically OCCUPIED, and - # the jump could not be made. - print("Jump move not applied: target turns out to be occupied.") - target_0.is_entangled = False - return iter(()) - # Otherwise we set target_0 to be classically EMPTY. - target_0.reset() - elif self.move_variant == MoveVariant.CLASSICAL: - if target_0.type_ != Type.EMPTY: - # For classical moves with target_0 occupied, we replace the qubit of target_0 with - # a new ancilla, and set its classical properties to be EMPTY. - world.unhook(target_0) - target_0.reset() + def is_split_move(self) -> bool: + return self.target2 is not None + + def is_merge_move(self) -> bool: + return self.source2 is not None + + def to_str(self, verbose_level: int = 1) -> str: + """Constructs the string representation of the move. + According to the value of verbose_level: + - 1: only returns the move source(s) and target(s); + - 2: additionally returns the move type and variant; + - 3: additionally returns the source(s) and target(s) piece type and color. + """ + if verbose_level < 1: + return "" + + if self.is_split_move(): + move_str = [self.source + "^" + self.target + str(self.target2)] + elif self.is_merge_move(): + move_str = [self.source + str(self.source2) + "^" + self.target] + else: + move_str = [self.source + self.target] + + if verbose_level > 1: + move_str.append(self.move_type.name) + move_str.append(self.move_variant.name) + + if verbose_level > 2: + source = self.board.board[self.source] + target = self.board.board[self.target] + move_str.append( + source.color.name + + "_" + + source.type_.name + + "->" + + target.color.name + + "_" + + target.type_.name + ) + return ":".join(move_str) - # Make the jump move. - 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() - return iter(()) + def __str__(self): + return self.to_str() class SplitJump(QuantumEffect): @@ -230,409 +160,3 @@ 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 - """ - - 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.") - 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: - 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: - # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player - # could try to do this move again. Is this desired? - 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. replaced with a new ancilla with value = 0. - 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 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 need to guarantee 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 is a non-zero probability - # that the source is not moved. - 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 - """ - - 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}") - if len(quantum_path_pieces_0) == 0: - # If there is no quantum path piece in path 0, flip the ancilla. - alpha.Flip()(path_0_clear_ancilla) - else: - # Otherwise 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}") - if len(quantum_path_pieces_1) == 0: - # If there is no quantum path piece in path 1, flip the ancilla. - alpha.Flip()(path_1_clear_ancilla) - else: - # Otherwise 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. - 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) - - # TODO(): Do we need to zero-out, i.e. reverse those ancillas? - - # 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. - 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 - """ - - 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. - 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. - """ - - 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) == 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: - 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: - # TODO(): in this case non of the path qubits are popped, i.e. the pieces are still entangled and the player - # could try to do this move again. Is this desired? - print("Cannon fire not applied: tthe path turns out to be blocked.") - return iter(()) - # Apply the capture. - # Quantumly reset the target. - world.unhook(target_0) - # Classically reset 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 - # TODO(): think a more efficient way of implementing this case. - # 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. - 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( - f"{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 a8bbcd24..a81510a2 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -38,991 +38,209 @@ ) from unitary.examples.quantum_chinese_chess.chess import * -# def test_move_eq(): -# board = Board.from_fen() -# move1 = Move( -# "a1", -# "b2", -# board, -# "c1", -# move_type=MoveType.MERGE_JUMP, -# move_variant=MoveVariant.CAPTURE, -# ) -# move2 = Move( -# "a1", -# "b2", -# board, -# "c1", -# move_type=MoveType.MERGE_JUMP, -# move_variant=MoveVariant.CAPTURE, -# ) -# move3 = Move( -# "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE -# ) -# move4 = Move( -# "a1", -# "b2", -# board, -# "c1", -# move_type=MoveType.MERGE_SLIDE, -# move_variant=MoveVariant.CAPTURE, -# ) -# assert move1 == move2 -# assert move1 != move3 -# assert move1 != move4 - - -# def test_move_type(): -# # TODO(): change to real senarios -# board = Board.from_fen() -# move1 = Move( -# "a1", -# "b2", -# board, -# "c1", -# move_type=MoveType.MERGE_JUMP, -# move_variant=MoveVariant.CAPTURE, -# ) -# assert move1.is_split_move() == False -# assert move1.is_merge_move() - -# move2 = Move( -# "a1", -# "b2", -# board, -# target2="c1", -# move_type=MoveType.SPLIT_JUMP, -# move_variant=MoveVariant.BASIC, -# ) -# assert move2.is_split_move() -# assert move2.is_merge_move() == False - -# move3 = Move( -# "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE -# ) -# assert move3.is_split_move() == False -# assert move3.is_merge_move() == False - - -# def test_to_str(): -# # TODO(): change to real scenarios -# board = Board.from_fen() -# move1 = Move( -# "a0", -# "a6", -# board, -# "c1", -# move_type=MoveType.MERGE_JUMP, -# move_variant=MoveVariant.CAPTURE, -# ) -# assert move1.to_str(0) == "" -# assert move1.to_str(1) == "a0c1^a6" -# assert move1.to_str(2) == "a0c1^a6:MERGE_JUMP:CAPTURE" -# assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:RED_ROOK->BLACK_PAWN" - -# move2 = Move( -# "a0", -# "b3", -# board, -# target2="c1", -# move_type=MoveType.SPLIT_JUMP, -# move_variant=MoveVariant.BASIC, -# ) -# assert move2.to_str(0) == "" -# assert move2.to_str(1) == "a0^b3c1" -# assert move2.to_str(2) == "a0^b3c1:SPLIT_JUMP:BASIC" -# assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:RED_ROOK->NA_EMPTY" - -# move3 = Move( -# "a0", "a6", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE -# ) -# assert move3.to_str(0) == "" -# assert move3.to_str(1) == "a0a6" -# assert move3.to_str(2) == "a0a6:SLIDE:CAPTURE" -# assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:RED_ROOK->BLACK_PAWN" - - -# def test_jump_classical(): -# # Target is empty. -# board = set_board(["a1", "b1"]) -# world = board.board -# # TODO(): try move all varaibles declarations of a1 = world["a1"] into a function. -# Jump(MoveVariant.CLASSICAL)(world["a1"], world["b2"]) -# assert_samples_in(board, [locations_to_bitboard(["b2", "b1"])]) - -# # Target is occupied. -# Jump(MoveVariant.CLASSICAL)(world["b2"], world["b1"]) -# assert_samples_in(board, [locations_to_bitboard(["b1"])]) - - -# def test_jump_capture(): -# # Source is in quantum state. -# board = set_board(["a1", "b1"]) -# world = board.board -# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2", "b1"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b1"])) -# Jump(MoveVariant.CAPTURE)(world["a2"], world["b1"]) -# # pop() will break the supersition and only one of the following two states are possible. -# # We check the ancilla to learn if the jump was applied or not. -# source_is_occupied = world.post_selection[world["ancilla_a2_0"]] -# if source_is_occupied: -# assert_samples_in(board, [locations_to_bitboard(["b1"])]) -# else: -# assert_samples_in(board, [locations_to_bitboard(["a3", "b1"])]) - -# # Target is in quantum state. -# board = set_board(["a1", "b1"]) -# world = board.board -# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) -# Jump(MoveVariant.CAPTURE)(world["a1"], world["b2"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) - -# # Both source and target are in quantum state. -# board = set_board(["a1", "b1"]) -# world = board.board -# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) -# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a2", "b2"]): 1 / 4.0, -# locations_to_bitboard(["a2", "b3"]): 1 / 4.0, -# locations_to_bitboard(["a3", "b2"]): 1 / 4.0, -# locations_to_bitboard(["a3", "b3"]): 1 / 4.0, -# }, -# ) -# Jump(MoveVariant.CAPTURE)(world["a2"], world["b2"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# # We check the ancilla to learn if the jump was applied or not. -# source_is_occupied = world.post_selection[world["ancilla_a2_0"]] -# print(source_is_occupied) -# if source_is_occupied: -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) -# else: -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b2"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b3"])) - - -# def test_jump_excluded(): -# # Target is in quantum state. -# board = set_board(["a1", "b1"]) -# world = board.board -# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) -# Jump(MoveVariant.EXCLUDED)(world["a1"], world["b2"]) -# # pop() will break the supersition and only one of the following two states are possible. -# # We check the ancilla to learn if the jump was applied or not. -# target_is_occupied = world.post_selection[world["ancilla_b2_0"]] -# print(target_is_occupied) -# if target_is_occupied: -# assert_samples_in(board, [locations_to_bitboard(["a1", "b2"])]) -# else: -# assert_samples_in(board, [locations_to_bitboard(["b2", "b3"])]) - -# # Both source and target are in quantum state. -# board = set_board(["a1", "b1"]) -# world = board.board -# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) -# alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) -# Jump(MoveVariant.EXCLUDED)(world["a2"], world["b2"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# # We check the ancilla to learn if the jump was applied or not. -# target_is_occupied = world.post_selection[world["ancilla_b2_0"]] -# print(target_is_occupied) -# if target_is_occupied: -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2", "b2"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b2"])) -# else: -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["b2", "b3"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3", "b3"])) - - -# def test_jump_basic(): -# # Source is in quantum state. -# board = set_board(["a1"]) -# world = board.board -# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) -# Jump(MoveVariant.BASIC)(world["a2"], world["d1"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["d1"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) - - -# def test_split_jump(): -# # Source is in classical state. -# board = set_board(["a1"]) -# world = board.board -# SplitJump()(world["a1"], world["a2"], world["a3"]) -# board_probabilities = get_board_probability_distribution(board, 1000) -# assert len(board_probabilities) == 2 -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) -# assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) - -# # Source is in quantum state. -# board = set_board(["a1"]) -# world = board.board -# alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) -# SplitJump()(world["a3"], world["a4"], world["a5"]) -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a2"]): 0.5, -# locations_to_bitboard(["a4"]): 0.25, -# locations_to_bitboard(["a5"]): 0.25, -# }, -# ) - - -# def test_merge_jump(): -# # Two quantum pieces split from one source could be merge back to one. -# board = set_board(["a1"]) -# world = board.board -# SplitJump()(world["a1"], world["a2"], world["a3"]) -# MergeJump()(world["a2"], world["a3"], world["a1"]) -# assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) - -# # Imperfect merge scenario 1 -# board = set_board(["a1"]) -# world = board.board -# SplitJump()(world["a1"], world["a2"], world["a3"]) -# SplitJump()(world["a3"], world["a4"], world["a5"]) -# # a2 has prob. 0.5 to be occupied, while a4 has prob. 0.25 to be occupied -# MergeJump()(world["a2"], world["a4"], world["a6"]) -# # Accoding to matrix calculations, the ending coefficient of -# # a5 to be occupied: -1/2; -# # a6 to be occupied: 1/2 + i/2/sqrt(2) -# # a4 to be occupied: -i/2 -1/2/sqrt(2) -# # a2 to be occupied: 0 -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a5"]): 1./4, -# locations_to_bitboard(["a6"]): 3./8, -# locations_to_bitboard(["a4"]): 3./8, -# }, -# ) - -# # Imperfect merge scenario 2 -# # Two quantum pieces split from two sources could not be merge into to one. -# board = set_board(["a1", "b1"]) -# world = board.board -# SplitJump()(world["a1"], world["a2"], world["a3"]) -# SplitJump()(world["b1"], world["b2"], world["b3"]) -# MergeJump()(world["a2"], world["b2"], world["c2"]) -# # According to matrix calculations, the ending coefficient of -# # [a3, b3]: 1/4 -# # [a3, c2]: i/2/sqrt(2) -# # [a3, b2]: -1/2/sqrt(2) -# # [b3, c2]: i/2/sqrt(2) -# # [b2, b3]: 1/2/sqrt(2) -# # [b2, c2]: 1/4 -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a3", "b3"]): 1./4, -# locations_to_bitboard(["a3", "c2"]): 1./8, -# locations_to_bitboard(["a3", "b2"]): 1./8, -# locations_to_bitboard(["b3", "c2"]): 1./8, -# locations_to_bitboard(["b2", "b3"]): 1./8, -# locations_to_bitboard(["b2", "c2"]): 1./4, -# }, -# ) - -# # Imperfect merge scenario 3 -# # This is a simplied version of the scenario above, where we unhook a3 and b3. -# board = set_board(["a1", "b1"]) -# world = board.board -# SplitJump()(world["a1"], world["a2"], world["a3"]) -# SplitJump()(world["b1"], world["b2"], world["b3"]) -# board.board.unhook(world["a3"]) -# board.board.unhook(world["b3"]) -# # Now the only quantum pieces in the board are a2 and b2. -# MergeJump()(world["a2"], world["b2"], world["c2"]) -# # The expected distribution is same as above by summing over a3 and b3. -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard([]): 1./4, -# locations_to_bitboard(["c2"]): 1./4, -# locations_to_bitboard(["b2"]): 1./4, -# locations_to_bitboard(["b2", "c2"]): 1./4, -# }, -# ) - - -# def test_slide_basic(): -# # 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./2, -# locations_to_bitboard(["b3", "c1"]): 1./2, # success -# }, -# ) - -# # 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./4, -# locations_to_bitboard(["a3", "b2"]): 1./4, -# locations_to_bitboard(["c1", "b3"]): 1./4, # success -# locations_to_bitboard(["a3", "b3"]): 1./4, -# }, -# ) - -# # 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./8, -# locations_to_bitboard(["a2", "b2", "c3"]): 1./8, -# locations_to_bitboard(["a2", "b3", "c2"]): 1./8, -# locations_to_bitboard(["d1", "b3", "c3"]): 1./8, # success -# locations_to_bitboard(["a3", "b2", "c2"]): 1./8, -# locations_to_bitboard(["a3", "b2", "c3"]): 1./8, -# locations_to_bitboard(["a3", "b3", "c2"]): 1./8, -# locations_to_bitboard(["a3", "b3", "c3"]): 1./8, -# }, -# ) - - -# def test_slide_excluded(): -# # 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./2, -# locations_to_bitboard(["a1", "b3", "c2"]): 1./2, -# }, -# ) -# else: -# # a1 could move to c2 if b2 is not there. -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a1", "b2", "c3"]): 1./2, -# locations_to_bitboard(["b3", "c2", "c3"]): 1./2, # success -# }, -# ) - -# # 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./4, -# locations_to_bitboard(["a3", "b3", "c2"]): 1./4, -# locations_to_bitboard(["a2", "b2", "c2"]): 1./4, -# locations_to_bitboard(["a2", "b3", "c2"]): 1./4, -# }, -# ) -# else: -# # a2 could move to c2 if b2 is not there. -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["a2", "b2", "c3"]): 1./4, -# locations_to_bitboard(["a3", "b2", "c3"]): 1./4, -# locations_to_bitboard(["b3", "c2", "c3"]): 1./4, # success -# locations_to_bitboard(["a3", "b3", "c3"]): 1./4, -# }, -# ) - -# # 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./8, -# locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1./8, -# locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1./8, -# locations_to_bitboard(["a2", "b3", "c3", "d2"]): 1./8, -# locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1./8, -# locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1./8, -# locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1./8, -# locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1./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./8, -# locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1./8, -# locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1./8, -# locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1./8, -# locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1./8, -# locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1./8, -# locations_to_bitboard(["d2", "b3", "c3", "d3"]): 1./8, # success -# locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1./8, -# }, -# ) - - -# def test_slide_capture(): -# # 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./2, -# locations_to_bitboard(["a1", "b2", "c3"]): 1./2, -# }, -# ) -# else: -# # a1 moves to c2. -# assert_sample_distribution( -# board, -# { -# locations_to_bitboard(["b3", "c2"]): 1./2, # slided and captured -# locations_to_bitboard(["b3", "c2", "c3"]): 1./2, # slided but not captured -# }, -# ) - -# # 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"]) +def test_move_eq(): + board = Board.from_fen() + move1 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + move2 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + move3 = Move( + "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE + ) + move4 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_SLIDE, + move_variant=MoveVariant.CAPTURE, + ) -# # 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", "c2", "d2"]): 1./2, # slided and captured -# locations_to_bitboard(["b3", "c2", "d2", "d3"]): 1./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./14, -# locations_to_bitboard(["a2", "b2", "c2", "d3"]): 1./14, -# locations_to_bitboard(["a2", "b2", "c3", "d2"]): 1./14, -# locations_to_bitboard(["a2", "b2", "c3", "d3"]): 1./14, -# locations_to_bitboard(["a2", "b3", "c2", "d2"]): 1./14, -# locations_to_bitboard(["a2", "b3", "c2", "d3"]): 1./14, -# locations_to_bitboard(["a3", "b2", "c2", "d2"]): 1./14, -# locations_to_bitboard(["a3", "b2", "c2", "d3"]): 1./14, -# locations_to_bitboard(["a3", "b2", "c3", "d2"]): 1./14, -# locations_to_bitboard(["a3", "b2", "c3", "d3"]): 1./14, -# locations_to_bitboard(["a3", "b3", "c2", "d2"]): 1./14, -# locations_to_bitboard(["a3", "b3", "c2", "d3"]): 1./14, -# # cases where the source is not there -# locations_to_bitboard(["a3", "b3", "c3", "d2"]): 1./14, -# locations_to_bitboard(["a3", "b3", "c3", "d3"]): 1./14, -# }, -# ) + assert move1 == move2 + assert move1 != move3 + assert move1 != move4 -def test_split_slide(): - # 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"]) +def test_move_type(): + # TODO(): change to real senarios + board = Board.from_fen() + move1 = Move( + "a1", + "b2", + board, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + assert move1.is_split_move() == False + assert move1.is_merge_move() - assert_sample_distribution( + move2 = Move( + "a1", + "b2", 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, - }, + target2="c1", + move_type=MoveType.SPLIT_JUMP, + move_variant=MoveVariant.BASIC, ) + assert move2.is_split_move() + assert move2.is_merge_move() == False - # 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"]) + move3 = Move( + "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE + ) + assert move3.is_split_move() == False + assert move3.is_merge_move() == False - SplitSlide(["b2", "c2"], ["d2"])(world["a2"], world["e1"], world["e2"]) - assert_sample_distribution( +def test_to_str(): + # TODO(): change to real scenarios + board = Board.from_fen() + move1 = Move( + "a0", + "a6", 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, - }, + "c1", + move_type=MoveType.MERGE_JUMP, + move_variant=MoveVariant.CAPTURE, + ) + assert move1.to_str(0) == "" + assert move1.to_str(1) == "a0c1^a6" + assert move1.to_str(2) == "a0c1^a6:MERGE_JUMP:CAPTURE" + assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:RED_ROOK->BLACK_PAWN" + + move2 = Move( + "a0", + "b3", + board, + target2="c1", + move_type=MoveType.SPLIT_JUMP, + move_variant=MoveVariant.BASIC, ) + assert move2.to_str(0) == "" + assert move2.to_str(1) == "a0^b3c1" + assert move2.to_str(2) == "a0^b3c1:SPLIT_JUMP:BASIC" + assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:RED_ROOK->NA_EMPTY" - # Source in quantum state + overlapped paths. - board = set_board(["a1", "b1", "c1"]) + move3 = Move( + "a0", "a6", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE + ) + assert move3.to_str(0) == "" + assert move3.to_str(1) == "a0a6" + assert move3.to_str(2) == "a0a6:SLIDE:CAPTURE" + assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:RED_ROOK->BLACK_PAWN" + + +def test_split_jump(): + # Source is in classical state. + board = set_board(["a1"]) 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"]) + board_probabilities = get_board_probability_distribution(board, 1000) + assert len(board_probabilities) == 2 + assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) + assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + # Source is in quantum state. + board = set_board(["a1"]) + world = board.board + alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["a3"], world["a4"], world["a5"]) 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, + locations_to_bitboard(["a2"]): 0.5, + locations_to_bitboard(["a4"]): 0.25, + locations_to_bitboard(["a5"]): 0.25, }, ) -def test_merge_slide(): - # One path is clear. - board = set_board(["a1", "b1"]) +def test_merge_jump(): + # Two quantum pieces split from one source could be merge back to one. + board = set_board(["a1"]) 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"]) + MergeJump()(world["a2"], world["a3"], world["a1"]) + assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + # Imperfect merge scenario 1 + board = set_board(["a1"]) + world = board.board + SplitJump()(world["a1"], world["a2"], world["a3"]) + SplitJump()(world["a3"], world["a4"], world["a5"]) + # a2 has prob. 0.5 to be occupied, while a4 has prob. 0.25 to be occupied + MergeJump()(world["a2"], world["a4"], world["a6"]) + # Accoding to matrix calculations, the ending coefficient of + # a5 to be occupied: -1/2; + # a6 to be occupied: 1/2 + i/2/sqrt(2) + # a4 to be occupied: -i/2 -1/2/sqrt(2) + # a2 to be occupied: 0 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, + locations_to_bitboard(["a5"]): 1.0 / 4, + locations_to_bitboard(["a6"]): 3.0 / 8, + locations_to_bitboard(["a4"]): 3.0 / 8, }, ) - # Multiple path qubits. - board = set_board(["a1", "b1", "c1", "d1"]) + # Imperfect merge scenario 2 + # Two quantum pieces split from two sources could not be merge into to one. + board = set_board(["a1", "b1"]) 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"]) - + MergeJump()(world["a2"], world["b2"], world["c2"]) + # According to matrix calculations, the ending coefficient of + # [a3, b3]: 1/4 + # [a3, c2]: i/2/sqrt(2) + # [a3, b2]: -1/2/sqrt(2) + # [b3, c2]: i/2/sqrt(2) + # [b2, b3]: 1/2/sqrt(2) + # [b2, c2]: 1/4 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 + locations_to_bitboard(["a3", "b3"]): 1.0 / 4, + locations_to_bitboard(["a3", "c2"]): 1.0 / 8, + locations_to_bitboard(["a3", "b2"]): 1.0 / 8, + locations_to_bitboard(["b3", "c2"]): 1.0 / 8, + locations_to_bitboard(["b2", "b3"]): 1.0 / 8, + locations_to_bitboard(["b2", "c2"]): 1.0 / 4, }, ) - # Overlapped paths. - board = set_board(["a1", "b1", "c1"]) + # Imperfect merge scenario 3 + # This is a simplied version of the scenario above, where we unhook a3 and b3. + board = set_board(["a1", "b1"]) 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"]) - + board.board.unhook(world["a3"]) + board.board.unhook(world["b3"]) + # Now the only quantum pieces in the board are a2 and b2. + MergeJump()(world["a2"], world["b2"], world["c2"]) + # The expected distribution is same as above by summing over a3 and b3. 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 + locations_to_bitboard([]): 1.0 / 4, + locations_to_bitboard(["c2"]): 1.0 / 4, + locations_to_bitboard(["b2"]): 1.0 / 4, + locations_to_bitboard(["b2", "c2"]): 1.0 / 4, }, ) - - -def test_cannon_fire(): - # 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}) - - # 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} - ) - - # 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} - ) - - # 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, - }, - ) - - -# TEST -def test_undo(monkeypatch): - inputs = iter(["y", "Bob", "Ben"]) - monkeypatch.setattr("builtins.input", lambda _: next(inputs)) - game = QuantumChineseChess() - board = set_board(["a1", "b1", "c1", "d1"]) - game.board = board - world = board.board - print("## Jump 1") - SplitJump()(world["a1"], world["a2"], world["a3"]) - print("## Jump 2") - SplitJump()(world["b1"], world["b2"], world["b3"]) - print("## Jump 3") - SplitJump()(world["c1"], world["c2"], world["c3"]) - print("## Jump 4") - SplitJump()(world["d1"], world["d2"], world["d3"]) - game.save_snapshot() - - print("## Fire ") - CannonFire([], ["b2", "c2"])(world["a2"], world["d2"]) - game.save_snapshot() - - # 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, - }, - ) - game.undo() - print("After undo.") - print(len(world.effect_history)) - assert 1 == 0 From 9c92e1d387d915309c498553f8daf91059f5aee4 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 10:12:54 -0700 Subject: [PATCH 17/31] update --- unitary/alpha/quantum_effect.py | 151 -------------------------------- 1 file changed, 151 deletions(-) delete mode 100644 unitary/alpha/quantum_effect.py diff --git a/unitary/alpha/quantum_effect.py b/unitary/alpha/quantum_effect.py deleted file mode 100644 index ee8779d1..00000000 --- a/unitary/alpha/quantum_effect.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2023 The Unitary Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from typing import Iterator, Optional, Sequence, Union -import abc -import enum - -import cirq - - -def _to_int(value: Union[enum.Enum, int]) -> int: - return value.value if isinstance(value, enum.Enum) else value - - -class QuantumEffect(abc.ABC): - @abc.abstractmethod - def effect(self, *objects) -> Iterator[cirq.Operation]: - """Apply the Quantum Effect to the QuantumObjects.""" - - def num_dimension(self) -> Optional[int]: - """Required Qid dimension. If any allowed, return None.""" - return None - - def num_objects(self) -> Optional[int]: - """Number of quantum objects allowed. - - If any allowed, return None. - """ - return None - - def _verify_objects(self, *objects): - if self.num_objects() is not None and len(objects) != self.num_objects(): - raise ValueError(f"Cannot apply effect to {len(objects)} qubits.") - - required_dimension = self.num_dimension() - for q in objects: - if (required_dimension is not None) and ( - q.num_states != required_dimension - ): - raise ValueError( - f"Cannot apply effect to qids of dimension {q.num_states}." - ) - if q.world is None: - raise ValueError( - "Object must be added to a QuantumWorld to apply effects." - ) - - def __call__(self, *objects): - """Apply the Quantum Effect to the objects.""" - self._verify_objects(*objects) - world = objects[0].world - effects = list(self.effect(*objects)) - if len(effects) > 0: - print("### call of QuantumEffect") - print(effects) - world.add_effect(effects) - print("\n\n") - - def __str__(self): - return self.__class__.__name__ - - -class QuantumIf: - """A `QuantumIf` effect allows quantum conditional effects. - - For conditional effects in a quantum world, a quantum if - can be used, which produces a controlled operation. - By using this in conjunction with a quantum object in - superposition, this can produce an entangled state. - - Example usage: - - QuantumIf(qubit).equals(state).apply(effect)(on_qubits) - - Note that the parameters to `apply` must be a quantum - effect. - - Multiple qubits can be set as the control by inputting - a list of qubits. However, the number of states (conditions) - must equal the number of control qubits. - """ - - def effect(self, *objects) -> Iterator[cirq.Operation]: - return iter(()) - - def __call__(self, *objects): - return QuantumThen(*objects) - - -class QuantumThen(QuantumEffect): - def __init__(self, *objects): - self.control_objects = list(objects) - self.condition = [1] * len(self.control_objects) - self.then_effect = None - - def equals( - self, *conditions: Union[enum.Enum, int, Sequence[Union[enum.Enum, int]]] - ) -> "QuantumThen": - """Allows a quantum if condition for qubits to equal certain states. - - Adding an equals after a quantum if can produce an anti-control - instead of a control if the condition is set to zero. - """ - # TODO: add qutrit support - if isinstance(conditions, (enum.Enum, int)): - conditions = [conditions] - if len(conditions) != len(self.control_objects): - raise ValueError( - f"Not able to equate {len(self.control_objects)} qubits with {len(conditions)} conditions" - ) - self.condition = [_to_int(cond) for cond in conditions] - return self - - def then(self, effect: "QuantumEffect"): - """Use `apply(effect)` instead.""" - return self.apply(effect) - - def apply(self, effect: "QuantumEffect"): - """Applies a QuantumEffect conditionally to the specified qubits.""" - self.then_effect = effect - return self - - def effect(self, *objects): - """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): - if cond == 0 and self.control_objects[idx].num_states == 2: - yield cirq.X(self.control_objects[idx].qubit) - - for op in self.then_effect.effect(*objects): - yield op.controlled_by(*[q.qubit for q in self.control_objects]) - - # For anti-controls, add an X after the controlled operation - # to revert its state back to what it was. - for idx, cond in enumerate(self.condition): - if cond == 0 and self.control_objects[idx].num_states == 2: - yield cirq.X(self.control_objects[idx].qubit) - - -quantum_if = QuantumIf() From 9aee85bf1494d013dcf924739b20609a76add5ef Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 10:20:34 -0700 Subject: [PATCH 18/31] revert --- unitary/alpha/quantum_effect.py | 146 ++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 unitary/alpha/quantum_effect.py diff --git a/unitary/alpha/quantum_effect.py b/unitary/alpha/quantum_effect.py new file mode 100644 index 00000000..f60623ba --- /dev/null +++ b/unitary/alpha/quantum_effect.py @@ -0,0 +1,146 @@ +# Copyright 2023 The Unitary Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import Iterator, Optional, Sequence, Union +import abc +import enum + +import cirq + + +def _to_int(value: Union[enum.Enum, int]) -> int: + return value.value if isinstance(value, enum.Enum) else value + + +class QuantumEffect(abc.ABC): + @abc.abstractmethod + def effect(self, *objects) -> Iterator[cirq.Operation]: + """Apply the Quantum Effect to the QuantumObjects.""" + + def num_dimension(self) -> Optional[int]: + """Required Qid dimension. If any allowed, return None.""" + return None + + def num_objects(self) -> Optional[int]: + """Number of quantum objects allowed. + + If any allowed, return None. + """ + return None + + def _verify_objects(self, *objects): + if self.num_objects() is not None and len(objects) != self.num_objects(): + raise ValueError(f"Cannot apply effect to {len(objects)} qubits.") + + required_dimension = self.num_dimension() + for q in objects: + if (required_dimension is not None) and ( + q.num_states != required_dimension + ): + raise ValueError( + f"Cannot apply effect to qids of dimension {q.num_states}." + ) + if q.world is None: + raise ValueError( + "Object must be added to a QuantumWorld to apply effects." + ) + + def __call__(self, *objects): + """Apply the Quantum Effect to the objects.""" + self._verify_objects(*objects) + world = objects[0].world + world.add_effect(list(self.effect(*objects))) + + def __str__(self): + return self.__class__.__name__ + + +class QuantumIf: + """A `QuantumIf` effect allows quantum conditional effects. + + For conditional effects in a quantum world, a quantum if + can be used, which produces a controlled operation. + By using this in conjunction with a quantum object in + superposition, this can produce an entangled state. + + Example usage: + + QuantumIf(qubit).equals(state).apply(effect)(on_qubits) + + Note that the parameters to `apply` must be a quantum + effect. + + Multiple qubits can be set as the control by inputting + a list of qubits. However, the number of states (conditions) + must equal the number of control qubits. + """ + + def effect(self, *objects) -> Iterator[cirq.Operation]: + return iter(()) + + def __call__(self, *objects): + return QuantumThen(*objects) + + +class QuantumThen(QuantumEffect): + def __init__(self, *objects): + self.control_objects = list(objects) + self.condition = [1] * len(self.control_objects) + self.then_effect = None + + def equals( + self, *conditions: Union[enum.Enum, int, Sequence[Union[enum.Enum, int]]] + ) -> "QuantumThen": + """Allows a quantum if condition for qubits to equal certain states. + + Adding an equals after a quantum if can produce an anti-control + instead of a control if the condition is set to zero. + """ + # TODO: add qutrit support + if isinstance(conditions, (enum.Enum, int)): + conditions = [conditions] + if len(conditions) != len(self.control_objects): + raise ValueError( + f"Not able to equate {len(self.control_objects)} qubits with {len(conditions)} conditions" + ) + self.condition = [_to_int(cond) for cond in conditions] + return self + + def then(self, effect: "QuantumEffect"): + """Use `apply(effect)` instead.""" + return self.apply(effect) + + def apply(self, effect: "QuantumEffect"): + """Applies a QuantumEffect conditionally to the specified qubits.""" + self.then_effect = effect + return self + + def effect(self, *objects): + """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): + if cond == 0 and self.control_objects[idx].num_states == 2: + yield cirq.X(self.control_objects[idx].qubit) + + for op in self.then_effect.effect(*objects): + yield op.controlled_by(*[q.qubit for q in self.control_objects]) + + # For anti-controls, add an X after the controlled operation + # to revert its state back to what it was. + for idx, cond in enumerate(self.condition): + if cond == 0 and self.control_objects[idx].num_states == 2: + yield cirq.X(self.control_objects[idx].qubit) + + +quantum_if = QuantumIf() From b64945e11623b943eb19c419d99881f093c161c4 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 10:27:05 -0700 Subject: [PATCH 19/31] update --- unitary/alpha/quantum_world.py | 60 +-- .../examples/quantum_chinese_chess/board.py | 234 ++------- .../quantum_chinese_chess/board_test.py | 256 ++++------ .../examples/quantum_chinese_chess/chess.py | 447 ++---------------- .../quantum_chinese_chess/chess_test.py | 280 +---------- .../examples/quantum_chinese_chess/enums.py | 24 +- 6 files changed, 178 insertions(+), 1123 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 365c3532..d5e6390d 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -248,17 +248,11 @@ def _compile_op(self, op: cirq.Operation) -> Union[cirq.Operation, cirq.OP_TREE] def add_effect(self, op_list: List[cirq.Operation]): """Adds an operation to the current circuit.""" - print("## Add_effect") - print(len(self.effect_history)) self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection)) ) for op in op_list: - print("### op") - print(op) self._append_op(op) - print("## Add_effect 2") - print(len(self.effect_history)) def undo_last_effect(self): """Restores the `QuantumWorld` to the state before the last effect. @@ -272,8 +266,6 @@ def undo_last_effect(self): if not self.effect_history: raise IndexError("No effects to undo") self.circuit, self.post_selection = self.effect_history.pop() - print("## undo_last_effect") - print(len(self.effect_history)) def _suggest_num_reps(self, sample_size: int) -> int: """Guess the number of raw samples needed to get sample_size results. @@ -313,20 +305,6 @@ def _interpret_result(self, result: Union[int, Iterable[int]]) -> int: return result_list[0] return result - def unhook(self, object: QuantumObject) -> None: - """Replaces all usages of the given object in the circuit with a new ancilla with value=0.""" - # Creates a new ancilla. - new_ancilla = self._add_ancilla(object.name) - # Replace operations using the qubit of the given object with the new ancilla. - qubit_remapping_dict = { - object.qubit: new_ancilla.qubit, - new_ancilla.qubit: object.qubit, - } - self.circuit = self.circuit.transform_qubits( - lambda q: qubit_remapping_dict.get(q, q) - ) - return - def force_measurement( self, obj: QuantumObject, result: Union[enum.Enum, int] ) -> None: @@ -445,8 +423,6 @@ def pop( objects: Optional[Sequence[Union[QuantumObject, str]]] = None, convert_to_enum: bool = True, ) -> List[Union[enum.Enum, int]]: - print("## pop") - print(len(self.effect_history)) self.effect_history.append( (self.circuit.copy(), copy.copy(self.post_selection)) ) @@ -460,8 +436,6 @@ def pop( results = self.peek(quantum_objects, convert_to_enum=convert_to_enum) for idx, result in enumerate(results[0]): self.force_measurement(quantum_objects[idx], result) - print("## pop 2") - print(len(self.effect_history)) return results[0] @@ -489,30 +463,6 @@ def get_histogram( histogram[idx][cast(int, result[idx])] += 1 return histogram - def get_histogram_with_whole_world_as_key(self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 - ) -> Dict[Tuple[int], int]: - """Creates histogram based on measurements (peeks) carried out. - - Parameters: - objects: List of QuantumObjects - count: Number of measurements - - Returns: - A dictionary, with the keys being each possible state of the whole quantum world - (or `objects` if specified), and the values being the count of that state. - """ - if not objects: - objects = self.public_objects - peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) - histogram = {} - for result in peek_results: - key = tuple(result) - if key not in histogram: - histogram[key] = 1 - else: - histogram[key] += 1 - return histogram - def get_probabilities( self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 ) -> List[Dict[int, float]]: @@ -556,11 +506,7 @@ def get_binary_probabilities( return binary_probs def __getitem__(self, name: str) -> QuantumObject: - try: - quantum_object = self.object_name_dict.get(name, None) - return quantum_object - except: - print("exsiting") - for obj in self.object_name_dict.keys(): - print(obj) + quantum_object = self.object_name_dict.get(name, None) + if not quantum_object: raise KeyError(f"{name} did not exist in this world.") + return quantum_object diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index c3d9525f..bcbe83b5 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -11,40 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np -from typing import List, Tuple +from typing import List import unitary.alpha as alpha from unitary.examples.quantum_chinese_chess.enums import ( SquareState, Color, Type, Language, - MoveVariant, ) from unitary.examples.quantum_chinese_chess.piece import Piece -from unitary.examples.quantum_chinese_chess.move import * -reset = "\033[0m" -bold = "\033[01m" -dim = "\033[02m" -negative = "\033[03m" -underline = "\033[04m" -blink = "\033[05m" -reverse = "\033[07m" -invisible = "\033[08m" -strikethrough = "\033[09m" - -# background -grey = "\033[47m" - -# foreground -black = "\033[30m" -red = "\033[31m" -green = "\033[32m" -lightred = "\033[91m" -lightgreen = "\033[92m" -lightgrey = "\033[37m" -darkgrey = "\033[90m" # The default initial state of the game. _INITIAL_FEN = "RHEAKAEHR/9/1C5C1/P1P1P1P1P/9/9/p1p1p1p1p/1c5c1/9/rheakaehr w---1" @@ -58,7 +34,6 @@ def __init__( ): self.board = board self.current_player = current_player - # This saves the locations of KINGs in the order of [RED_KING_LOCATION, BLACK_KING_LOCATION]. self.king_locations = king_locations self.lang = Language.EN # The default language is English. @@ -72,7 +47,7 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": FEN rule for Chinese Chess could be found at https://www.wxf-xiangqi.org/images/computer-xiangqi/fen-for-xiangqi-chinese-chess.pdf """ chess_board = {} - row_index = 0 + row_index = 9 king_locations = [] pieces, turns = fen.split(" ", 1) for row in pieces.split("/"): @@ -97,203 +72,50 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": name, SquareState.OCCUPIED, piece_type, color ) col += 1 - row_index += 1 + row_index -= 1 board = alpha.QuantumWorld(chess_board.values()) # Here 0 means the player RED while 1 the player BLACK. current_player = 0 if "w" in turns else 1 - # TODO(): maybe add check to make sure the input fen itself is correct. return cls(board, current_player, king_locations) - def to_str( - self, - probabilities: List[float] = None, - print_probabilities=True, - peek_result: List[int] = None, - ): - # TODO(): print players' names in their corresponding side of the board. + def __str__(self): num_rows = 10 - board_string = ["\n "] - if print_probabilities and probabilities is None: - probabilities = self.board.get_binary_probabilities() + board_string = ["\n "] # Print the top line of col letters. - board_string += grey - board_string += black for col in "abcdefghi": - if self.lang == Language.EN: - board_string.append(f" {col} ") - else: - board_string.append(f"{col} ") - board_string += "\b" + reset + " \n" - index = 0 + board_string.append(f" {col}") + board_string.append("\n") for row in range(num_rows): # Print the row index on the left. - if self.lang == Language.EN: - board_string.append(f"{row} ") - else: - board_string.append(f"{row} ") + board_string.append(f"{row} ") for col in "abcdefghi": piece = self.board[f"{col}{row}"] - # board_string += grey - if peek_result is None and piece.is_entangled: - if piece.color == Color.RED: - board_string += lightred - else: - board_string += lightgrey - else: - # board_string += bold - if piece.color == Color.RED: - board_string += red - else: - # board_string += black - pass - if ( - peek_result is None - or piece.type_ == Type.EMPTY - or peek_result[index] == 1 - ): - board_string += piece.symbol(self.lang) - elif piece.is_entangled and peek_result[index] == 0: - board_string += Type.symbol(Type.EMPTY, Color.NA, self.lang) - if col != "i": - if self.lang == Language.EN: - board_string.append(" ") - else: - board_string.append(" ") - board_string += reset - index += 1 + board_string += piece.symbol(self.lang) + if self.lang == Language.EN: + board_string.append(" ") # Print the row index on the right. - board_string.append(f" {row}\n") - # Print the sampled prob. of the pieces in the above row. - if print_probabilities: - board_string += " " - board_string += grey - board_string += black - for i in range(row * 9, (row + 1) * 9): - board_string.append("{:.1f} ".format(probabilities[i])) - board_string += "\b" + reset + " \n" - board_string.append(" ") + board_string.append(f" {row}\n") + board_string.append(" ") # Print the bottom line of col letters. - board_string += grey - board_string += black for col in "abcdefghi": - if self.lang == Language.EN: - board_string.append(f" {col} ") - else: - board_string.append(f"{col} ") - board_string += "\b" + reset + " \n" + board_string.append(f" {col}") + board_string.append("\n") if self.lang == Language.EN: return "".join(board_string) # We need to turn letters into their full-width counterparts to align # a mix of letters + Chinese characters. - half_width_chars = "".join( - [" ", "."] - + [chr(c) for c in range(ord("A"), ord("Z"))] - + [chr(c) for c in range(ord("a"), ord("z"))] + chars = "".join(chr(c) for c in range(ord(" "), ord("z"))) + full_width_chars = "\N{IDEOGRAPHIC SPACE}" + "".join( + chr(c) + for c in range( + ord("\N{FULLWIDTH EXCLAMATION MARK}"), + ord("\N{FULLWIDTH LATIN SMALL LETTER Z}"), + ) ) - full_width_chars = "".join( - ["\N{IDEOGRAPHIC SPACE}", "\u30FB"] - + [ - chr(c) - for c in range( - ord("\N{FULLWIDTH LATIN CAPITAL LETTER A}"), - ord("\N{FULLWIDTH LATIN CAPITAL LETTER Z}"), - ) - ] - + [ - chr(c) - for c in range( - ord("\N{FULLWIDTH LATIN SMALL LETTER A}"), - ord("\N{FULLWIDTH LATIN SMALL LETTER Z}"), - ) - ] + translation = str.maketrans(chars, full_width_chars) + return ( + "".join(board_string) + .replace(" ", "") + .replace("abcdefghi", " abcdefghi") + .translate(translation) ) - translation = str.maketrans(half_width_chars, full_width_chars) - return "".join(board_string).translate(translation) - - def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: - """Returns the nonempty classical and quantum pieces from source to target (excluded).""" - x0 = ord(source[0]) - x1 = ord(target[0]) - dx = x1 - x0 - y0 = int(source[1]) - y1 = int(target[1]) - dy = y1 - y0 - # In case of only moving one step, return empty path pieces. - if abs(dx) + abs(dy) <= 1: - return [], [] - # In case of advisor moving, return empty path pieces. - # TODO(): maybe move this to the advisor move check. - if abs(dx) == 1 and abs(dy) == 1: - return [], [] - pieces = [] - classical_pieces = [] - quantum_pieces = [] - dx_sign = np.sign(dx) - dy_sign = np.sign(dy) - # In case of elephant move, there should only be one path piece. - if abs(dx) == abs(dy): - pieces.append(f"{chr(x0 + dx_sign)}{y0 + dy_sign}") - # This could be move of rook, king, pawn or cannon. - elif dx == 0: - for i in range(1, abs(dy)): - pieces.append(f"{chr(x0)}{y0 + dy_sign * i}") - # This could be move of rook, king, pawn or cannon. - elif dy == 0: - for i in range(1, abs(dx)): - pieces.append(f"{chr(x0 + dx_sign * i)}{y0}") - # This covers four possible directions of horse move. - elif abs(dx) == 2 and abs(dy) == 1: - pieces.append(f"{chr(x0 + dx_sign)}{y0}") - # This covers the other four possible directions of horse move. - elif abs(dy) == 2 and abs(dx) == 1: - pieces.append(f"{chr(x0)}{y0 + dy_sign}") - else: - raise ValueError("The input move is illegal.") - for piece in pieces: - if self.board[piece].is_entangled: - quantum_pieces.append(piece) - elif self.board[piece].type_ != Type.EMPTY: - classical_pieces.append(piece) - return classical_pieces, quantum_pieces - - def flying_general_check(self) -> bool: - """Check and return if the two KINGs are directly facing each other (i.e. in the same column) without any pieces in between.""" - king_0 = self.king_locations[0] - king_1 = self.king_locations[1] - if king_0[0] != king_1[0]: - # If they are in different columns, the check fails. Game continues. - return False - classical_pieces, quantum_pieces = self.path_pieces(king_0, king_1) - if len(classical_pieces) > 0: - # If there are classical pieces between two KINGs, the check fails. Game continues. - return False - if len(quantum_pieces) == 0: - # If there are no pieces between two KINGs, the check successes. Game ends. - return True - # When there are quantum pieces in between, the check successes (and game ends) - # if there are at lease one occupied path piece. - capture_ancilla = self.board._add_ancilla("flying_general_check") - control_objects = [self.board[path] for path in quantum_pieces] - conditions = [0] * len(control_objects) - alpha.quantum_if(*control_objects).equals(*conditions).apply(alpha.Flip())( - capture_ancilla - ) - could_capture = self.board.pop([capture_ancilla])[0] - if could_capture: - # Force measure all path pieces to be empty. - for path_piece in control_objects: - self.board.force_measurement(path_piece, 0) - path_piece.reset() - - # Let the general/king fly, i.e. the opposite king will capture the current king. - current_king = self.board[self.king_locations[self.current_player]] - oppsite_king = self.board[self.king_locations[1 - self.current_player]] - Jump(MoveVariant.CLASSICAL)(oppsite_king, current_king) - print("==== FLYING GENERAL ! ====") - return True - else: - # TODO(): we are leaving the path pieces unchanged in entangled state. Maybe - # better to force measure them? One option is to randomly chosing one path piece - # and force measure it to be occupied. - print("==== General not flies yet ! ====") - return False diff --git a/unitary/examples/quantum_chinese_chess/board_test.py b/unitary/examples/quantum_chinese_chess/board_test.py index b6cf37e7..0ba0e73f 100644 --- a/unitary/examples/quantum_chinese_chess/board_test.py +++ b/unitary/examples/quantum_chinese_chess/board_test.py @@ -12,178 +12,90 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from unitary.examples.quantum_chinese_chess.enums import ( - Language, - Color, - Type, - SquareState, -) -from unitary.examples.quantum_chinese_chess.board import * -from unitary.examples.quantum_chinese_chess.piece import Piece -from unitary.examples.quantum_chinese_chess.test_utils import ( - locations_to_bitboard, - assert_samples_in, - assert_sample_distribution, - get_board_probability_distribution, - set_board, -) +from unitary.examples.quantum_chinese_chess.enums import Language +from unitary.examples.quantum_chinese_chess.board import Board -# def test_init_with_default_fen(): -# board = Board.from_fen() -# assert ( -# board.__str__() -# == """ -# a b c d e f g h i -# 9 r h e a k a e h r 9 -# 8 . . . . . . . . . 8 -# 7 . c . . . . . c . 7 -# 6 p . p . p . p . p 6 -# 5 . . . . . . . . . 5 -# 4 . . . . . . . . . 4 -# 3 P . P . P . P . P 3 -# 2 . C . . . . . C . 2 -# 1 . . . . . . . . . 1 -# 0 R H E A K A E H R 0 -# a b c d e f g h i -# """ -# ) - -# board.set_language(Language.ZH) -# assert ( -# board.__str__() -# == """ -#  abcdefghi -# 9車馬相仕帥仕相馬車9 -# 8.........8 -# 7.砲.....砲.7 -# 6卒.卒.卒.卒.卒6 -# 5.........5 -# 4.........4 -# 3兵.兵.兵.兵.兵3 -# 2.炮.....炮.2 -# 1.........1 -# 0车马象士将士象马车0 -#  abcdefghi -# """ -# ) - -# assert board.king_locations == ["e0", "e9"] - - -# def test_init_with_specified_fen(): -# board = Board.from_fen("4kaR2/4a4/3hR4/7H1/9/9/9/9/4Ap1r1/3AK3c w---1 ") - -# assert ( -# board.__str__() -# == """ -# a b c d e f g h i -# 9 . . . A K . . . c 9 -# 8 . . . . A p . r . 8 -# 7 . . . . . . . . . 7 -# 6 . . . . . . . . . 6 -# 5 . . . . . . . . . 5 -# 4 . . . . . . . . . 4 -# 3 . . . . . . . H . 3 -# 2 . . . h R . . . . 2 -# 1 . . . . a . . . . 1 -# 0 . . . . k a R . . 0 -# a b c d e f g h i -# """ -# ) - -# board.set_language(Language.ZH) -# assert ( -# board.__str__() -# == """ -#  abcdefghi -# 9...士将...砲9 -# 8....士卒.車.8 -# 7.........7 -# 6.........6 -# 5.........5 -# 4.........4 -# 3.......马.3 -# 2...馬车....2 -# 1....仕....1 -# 0....帥仕车..0 -#  abcdefghi -# """ -# ) - -# assert board.king_locations == ["e0", "e9"] - - -def test_path_pieces(): +def test_init_with_default_fen(): board = Board.from_fen() - # In case of only moving one step, return empty path pieces. - assert board.path_pieces("a0", "a1") == ([], []) - - # In case of advisor moving, return empty path pieces. - assert board.path_pieces("d0", "e1") == ([], []) - - # In case of elephant move, there should be at most one path piece. - assert board.path_pieces("c0", "e2") == ([], []) - # Add one classical piece in the path. - board.board["d1"].reset(Piece("d1", SquareState.OCCUPIED, Type.ROOK, Color.RED)) - assert board.path_pieces("c0", "e2") == (["d1"], []) - # Add one quantum piece in the path. - board.board["d1"].is_entangled = True - assert board.path_pieces("c0", "e2") == ([], ["d1"]) - - # Horizontal move - board.board["c7"].reset(Piece("c7", SquareState.OCCUPIED, Type.ROOK, Color.RED)) - board.board["c7"].is_entangled = True - assert board.path_pieces("a7", "i7") == (["b7", "h7"], ["c7"]) - - # Vertical move - assert board.path_pieces("c0", "c9") == (["c3", "c6"], ["c7"]) - - # In case of horse move, there should be at most one path piece. - assert board.path_pieces("b9", "a7") == ([], []) - assert board.path_pieces("b9", "c7") == ([], []) - # One classical piece in path. - assert board.path_pieces("b9", "d8") == (["c9"], []) - # One quantum piece in path. - assert board.path_pieces("c8", "d6") == ([], ["c7"]) - - -def test_flying_general_check(): - board = Board.from_fen() - # If they are in different columns, the check fails. - board.king_locations = ["d0", "e9"] - assert board.flying_general_check() == False - - # If there are classical pieces between two KINGs, the check fails. - board.king_locations = ["e0", "e9"] - assert board.flying_general_check() == False - - # If there are no pieces between two KINGs, the check successes. - board.board["e3"].reset() - board.board["e6"].reset() - assert board.flying_general_check() == True - - # When there are quantum pieces in between. - board = set_board(["a3", "a4", "e0", "e9"]) - board.king_locations = ["e0", "e9"] - board.current_player = 0 # i.e. RED - world = board.board - SplitJump()(world["a3"], world["c3"], world["e3"]) - SplitJump()(world["a4"], world["c4"], world["e4"]) - - result = board.flying_general_check() - # We check the ancilla to learn whether the general/king flies or not. - captured = world.post_selection[world["ancilla_ancilla_flying_general_check_0_0"]] - if captured: - assert result - assert_samples_in(board, {locations_to_bitboard(["c3", "c4", "e0"]): 1.0}) - else: - assert not result - assert_sample_distribution( - board, - { - locations_to_bitboard(["e0", "e9", "e3", "e4"]): 1.0 / 3, - locations_to_bitboard(["e0", "e9", "e3", "c4"]): 1.0 / 3, - locations_to_bitboard(["e0", "e9", "c3", "e4"]): 1.0 / 3, - }, - ) + assert ( + board.__str__() + == """ + a b c d e f g h i +0 r h e a k a e h r 0 +1 . . . . . . . . . 1 +2 . c . . . . . c . 2 +3 p . p . p . p . p 3 +4 . . . . . . . . . 4 +5 . . . . . . . . . 5 +6 P . P . P . P . P 6 +7 . C . . . . . C . 7 +8 . . . . . . . . . 8 +9 R H E A K A E H R 9 + a b c d e f g h i +""" + ) + + board.set_language(Language.ZH) + assert ( + board.__str__() + == """ + abcdefghi +0車馬相仕帥仕相馬車0 +1.........1 +2.砲.....砲.2 +3卒.卒.卒.卒.卒3 +4.........4 +5.........5 +6兵.兵.兵.兵.兵6 +7.炮.....炮.7 +8.........8 +9车马象士将士象马车9 + abcdefghi +""" + ) + + assert board.king_locations == ["e9", "e0"] + + +def test_init_with_specified_fen(): + board = Board.from_fen("4kaR2/4a4/3hR4/7H1/9/9/9/9/4Ap1r1/3AK3c w---1 ") + + assert ( + board.__str__() + == """ + a b c d e f g h i +0 . . . A K . . . c 0 +1 . . . . A p . r . 1 +2 . . . . . . . . . 2 +3 . . . . . . . . . 3 +4 . . . . . . . . . 4 +5 . . . . . . . . . 5 +6 . . . . . . . H . 6 +7 . . . h R . . . . 7 +8 . . . . a . . . . 8 +9 . . . . k a R . . 9 + a b c d e f g h i +""" + ) + + board.set_language(Language.ZH) + assert ( + board.__str__() + == """ + abcdefghi +0...士将...砲0 +1....士卒.車.1 +2.........2 +3.........3 +4.........4 +5.........5 +6.......马.6 +7...馬车....7 +8....仕....8 +9....帥仕车..9 + abcdefghi +""" + ) + + assert board.king_locations == ["e9", "e0"] diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index d04724a8..bca5db3d 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -13,24 +13,8 @@ # limitations under the License. from typing import Tuple, List from unitary.examples.quantum_chinese_chess.board import Board -from unitary.examples.quantum_chinese_chess.enums import ( - Language, - GameState, - Type, - Color, - MoveType, - MoveVariant, -) -from unitary.examples.quantum_chinese_chess.move import ( - Jump, - Slide, - SplitJump, - SplitSlide, - MergeJump, - MergeSlide, - CannonFire, -) -import readline +from unitary.examples.quantum_chinese_chess.enums import Language, GameState, Type +from unitary.examples.quantum_chinese_chess.move import Move # List of accepable commands. _HELP_TEXT = """ @@ -51,38 +35,26 @@ class QuantumChineseChess: """A class that implements Quantum Chinese Chess using the unitary API.""" - def print_welcome(self) -> None: - """Prints the welcome message. Gets board language and players' name.""" - print(_WELCOME_MESSAGE) - print(_HELP_TEXT) - # TODO(): add the whole set of Chinese interface support. - lang = input( - "Switch to Chinese board characters? (y/n) (default to be English) " - ) - if lang.lower() == "y": - self.lang = Language.ZH - else: - self.lang = Language.EN - name_0 = input("Player 0's name (default to be Player_0): ") - self.players_name.append("Player_0" if len(name_0) == 0 else name_0) - name_1 = input("Player 1's name (default to be Player_1): ") - self.players_name.append("Player_1" if len(name_1) == 0 else name_1) - def __init__(self): self.players_name = [] self.print_welcome() self.board = Board.from_fen() self.board.set_language(self.lang) - print(self.board.to_str()) + print(self.board) self.game_state = GameState.CONTINUES self.current_player = self.board.current_player self.debug_level = 3 - # This variable is used to save the length of current effect history before each move is made, - # so that if we later undo we know how many effects we need to pop out. - self.effect_history_length = [] - # This variable is used to save the classical properties of the whole board before each move is - # made, so that if we later undo we could recover the earlier classical state. - self.classical_properties_history = [] + + def game_over(self) -> None: + """Checks if the game is over, and update self.game_state accordingly.""" + if self.game_state != GameState.CONTINUES: + return + return + # TODO(): add the following checks + # - The current player wins if general is captured in the current move. + # - The other player wins if the flying general rule is satisfied, i.e. there is no piece + # (after measurement) between two generals. + # - If player 0 made N repeatd back-and_forth moves in a row. @staticmethod def parse_input_string(str_to_parse: str) -> Tuple[List[str], List[str]]: @@ -133,396 +105,56 @@ def parse_input_string(str_to_parse: str) -> Tuple[List[str], List[str]]: ) return sources, targets - def check_classical_rule( - self, source: str, target: str, classical_path_pieces: List[str] - ) -> None: - """Check if the proposed move satisfies classical rules, and raises ValueError if not.""" - source_piece = self.board.board[source] - target_piece = self.board.board[target] - # Check if the move is blocked by classical path piece. - if len(classical_path_pieces) > 0 and source_piece.type_ != Type.CANNON: - # The path is blocked by classical pieces. - raise ValueError("The path is blocked.") - - # Check if the target has classical piece of the same color. - if not target_piece.is_entangled and source_piece.color == target_piece.color: - raise ValueError( - "The target place has classical piece with the same color." - ) - - # Check if the move violates any classical rule. - x0 = ord(source[0]) - x1 = ord(target[0]) - dx = x1 - x0 - y0 = int(source[1]) - y1 = int(target[1]) - dy = y1 - y0 - - if source_piece.type_ == Type.ROOK: - if dx != 0 and dy != 0: - raise ValueError("ROOK cannot move like this.") - elif source_piece.type_ == Type.HORSE: - if not ((abs(dx) == 2 and abs(dy) == 1) or (abs(dx) == 1 and abs(dy) == 2)): - raise ValueError("HORSE cannot move like this.") - elif source_piece.type_ == Type.ELEPHANT: - if not (abs(dx) == 2 and abs(dy) == 2): - raise ValueError("ELEPHANT cannot move like this.") - if (source_piece.color == Color.RED and y1 < 5) or ( - source_piece.color == Color.BLACK and y1 > 4 - ): - raise ValueError( - "ELEPHANT cannot cross the river (i.e. the middle line)." - ) - elif source_piece.type_ == Type.ADVISOR: - if not (abs(dx) == 1 and abs(dy) == 1): - raise ValueError("ADVISOR cannot move like this.") - if ( - x1 > ord("f") - or x1 < ord("d") - or (source_piece.color == Color.RED and y1 < 7) - or (source_piece.color == Color.BLACK and y1 > 2) - ): - raise ValueError("ADVISOR cannot leave the palace.") - elif source_piece.type_ == Type.KING: - if abs(dx) + abs(dy) != 1: - raise ValueError("KING cannot move like this.") - if ( - x1 > ord("f") - or x1 < ord("d") - or (source_piece.color == Color.RED and y1 < 7) - or (source_piece.color == Color.BLACK and y1 > 2) - ): - raise ValueError("KING cannot leave the palace.") - elif source_piece.type_ == Type.CANNON: - if dx != 0 and dy != 0: - raise ValueError("CANNON cannot move like this.") - if len(classical_path_pieces) > 0: - if len(classical_path_pieces) > 1: - # Invalid cannon move, since there could only be at most one classical piece between - # the source (i.e. the cannon) and the target. - raise ValueError("CANNON cannot fire like this.") - elif source_piece.color == target_piece.color: - raise ValueError("CANNON cannot fire to a piece with same color.") - elif target_piece.color == Color.NA: - raise ValueError("CANNON cannot fire to an empty piece.") - elif source_piece.type_ == Type.PAWN: - if abs(dx) + abs(dy) != 1: - raise ValueError("PAWN cannot move like this.") - if source_piece.color == Color.RED: - if dy == 1: - raise ValueError("PAWN can not move backward.") - if y0 > 4 and dy != -1: - raise ValueError( - "PAWN can only go forward before crossing the river (i.e. the middle line)." - ) - else: - if dy == -1: - raise ValueError("PAWN can not move backward.") - if y0 <= 4 and dy != 1: - raise ValueError( - "PAWN can only go forward before crossing the river (i.e. the middle line)." - ) - - def classify_move( - self, - sources: List[str], - targets: List[str], - classical_path_pieces_0: List[str], - quantum_path_pieces_0: List[str], - classical_path_pieces_1: List[str], - quantum_path_pieces_1: List[str], - ) -> Tuple[MoveType, MoveVariant]: - """Determines the MoveType and MoveVariant.""" - move_type = MoveType.UNSPECIFIED - move_variant = MoveVariant.UNSPECIFIED - - source = self.board.board[sources[0]] - target = self.board.board[targets[0]] - - if len(sources) == 1 and len(targets) == 1: - if len(quantum_path_pieces_0) == 0: - if ( - len(classical_path_pieces_0) == 0 - and source.type_ == Type.CANNON - and target.color.value == 1 - source.color.value - ): - raise ValueError( - "CANNON could not fire/capture without a cannon platform." - ) - if not source.is_entangled and not target.is_entangled: - return MoveType.CLASSICAL, MoveVariant.CLASSICAL - else: - move_type = MoveType.JUMP - else: - move_type = MoveType.SLIDE - - if ( - source.type_ == Type.CANNON - and ( - len(classical_path_pieces_0) == 1 or len(quantum_path_pieces_0) > 0 - ) - and target.color.value == 1 - source.color.value - ): - # By this time the classical cannon fire has been identified as CLASSICAL JUMP. - return MoveType.CANNON_FIRE, MoveVariant.CAPTURE - # Determine MoveVariant. - if target.color == Color.NA: - move_variant = MoveVariant.BASIC - # TODO(): such move could be a merge. Take care of such cases later. - elif target.color == source.color: - move_variant = MoveVariant.EXCLUDED - else: - move_variant = MoveVariant.CAPTURE - - elif len(sources) == 2: - source_1 = self.board.board[sources[1]] - if not source.is_entangled or not source_1.is_entangled: - raise ValueError( - "Both sources need to be in quantum state in order to merge." - ) - # TODO(): Currently we don't support merge + excluded/capture, or cannon_merge_fire + capture. Maybe add support later. - if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: - raise ValueError("Currently CANNON could not merge while fire.") - if target.type_ != Type.EMPTY: - raise ValueError("Currently we could only merge into an empty piece.") - if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: - move_type = MoveType.MERGE_JUMP - else: - move_type = MoveType.MERGE_SLIDE - move_variant = MoveVariant.BASIC - - elif len(targets) == 2: - target_1 = self.board.board[targets[1]] - # TODO(): Currently we don't support split + excluded/capture, or cannon_split_fire + capture. Maybee add support later. - if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: - raise ValueError("Currently CANNON could not split while fire.") - if target.type_ != Type.EMPTY or target_1.type_ != Type.EMPTY: - raise ValueError("Currently we could only split into empty pieces.") - if source.type_ == Type.KING: - # TODO(): Currently we don't support KING split. Maybe add support later. - raise ValueError("King split is not supported currently.") - if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: - move_type = MoveType.SPLIT_JUMP - else: - move_type = MoveType.SPLIT_SLIDE - move_variant = MoveVariant.BASIC - return move_type, move_variant - def apply_move(self, str_to_parse: str) -> None: """Check if the input string is valid. If it is, determine the move type and variant and return the move.""" - sources, targets = self.parse_input_string(str_to_parse) - + try: + sources, targets = self.parse_input_string(str_to_parse) + except ValueError as e: + raise e # Additional checks based on the current board. for source in sources: if self.board.board[source].type_ == Type.EMPTY: raise ValueError("Could not move empty piece.") if self.board.board[source].color.value != self.board.current_player: raise ValueError("Could not move the other player's piece.") - source_0 = self.board.board[sources[0]] - target_0 = self.board.board[targets[0]] - if len(sources) == 2: - source_1 = self.board.board[sources[1]] - if source_0.type_ != source_1.type_: - raise ValueError("Two sources need to be the same type.") - if len(targets) == 2: - target_1 = self.board.board[targets[1]] - # TODO(): handle the case where a piece is split into the current piece and another piece, in which case two targets are different. - if target_0.type_ != target_1.type_: - raise ValueError("Two targets need to be the same type.") - if target_0.color != target_1.color: - raise ValueError("Two targets need to be the same color.") - - # Check if the first path satisfies the classical rule. - classical_pieces_0, quantum_pieces_0 = self.board.path_pieces( - sources[0], targets[0] - ) - self.check_classical_rule(sources[0], targets[0], classical_pieces_0) - - # Check if the second path (if exists) satisfies the classical rule. - classical_pieces_1 = None - quantum_pieces_1 = None + # TODO(): add analysis to determine move type and variant. - if len(sources) == 2: - classical_pieces_1, quantum_pieces_1 = self.board.path_pieces( - sources[1], targets[0] - ) - self.check_classical_rule(sources[1], targets[0], classical_pieces_1) - elif len(targets) == 2: - classical_pieces_1, quantum_pieces_1 = self.board.path_pieces( - sources[0], targets[1] - ) - self.check_classical_rule(sources[0], targets[1], classical_pieces_1) - # Classify the move type and move variant. - move_type, move_variant = self.classify_move( - sources, - targets, - classical_pieces_0, - quantum_pieces_0, - classical_pieces_1, - quantum_pieces_1, - ) - - print(move_type, " ", move_variant) - - if move_type == MoveType.CLASSICAL: - if source_0.type_ == Type.KING: - # Update the locations of KING. - self.board.king_locations[self.current_player] = targets[0] - # TODO(): only make such prints for a certain debug level. - print(f"Updated king locations: {self.board.king_locations}.") - if target_0.type_ == Type.KING: - # King is captured, then the game is over. - self.game_state = GameState(self.current_player) - Jump(move_variant)(source_0, target_0) - elif move_type == MoveType.JUMP: - Jump(move_variant)(source_0, target_0) - 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]: + def next_move(self) -> bool: """Check if the player wants to exit or needs help message. Otherwise parse and apply the move. - Returns True + output string if the move was made, otherwise returns False + output string. + Returns True if the move was made, otherwise returns False. """ input_str = input( f"\nIt is {self.players_name[self.current_player]}'s turn to move: " ) - output = "" if input_str.lower() == "help": - output = _HELP_TEXT + print(_HELP_TEXT) elif input_str.lower() == "exit": # The other player wins if the current player quits. self.game_state = GameState(1 - self.current_player) - output = "Exiting." - elif input_str.lower() == "peek": - # TODO(): make it look like the normal board. Right now it's only for debugging purposes. - print( - self.board.to_str( - None, False, self.board.board.peek(convert_to_enum=False)[0] - ) - ) - elif input_str.lower() == "peek all": - all_boards = self.board.board.get_histogram_with_whole_world_as_key() - sorted_boards = sorted(all_boards.items(), key=lambda x: x[1], reverse=True) - for board, count in sorted_boards: - print( - "\n ====== With probability ~ {:.1f} ======".format(count / 100.0) - ) - print(self.board.to_str(None, False, list(board))) - elif input_str.lower() == "undo": - if len(self.effect_history_length) <= 1: - # length == 1 corresponds to the initial state, and no more undo could be made. - return False, "Unable to undo any more." - self.undo() - return True, "Undoing." + print("Exiting.") else: try: # The move is success if no ValueError is raised. self.apply_move(input_str.lower()) - return True, output + return True except ValueError as e: - output = f"Invalid move. {e}" - return False, output - - def update_board_by_sampling(self) -> List[float]: - probs = self.board.board.get_binary_probabilities() - num_rows = 10 - num_cols = 9 - for row in range(num_rows - 1, -1, -1): - for col in "abcdefghi": - piece = self.board.board[f"{col}{row}"] - prob = probs[row * num_cols + ord(col) - ord("a")] - # TODO(): set more accurate threshold - if prob < 1e-3: - piece.reset() - elif prob > 1 - 1e-3: - piece.is_entangled = False - - def game_over(self) -> None: - """Checks if the game is over, and update self.game_state accordingly.""" - if self.game_state != GameState.CONTINUES: - return - if self.board.flying_general_check(): - # If two KINGs are directly facing each other (i.e. in the same column) without any pieces in between, then the game ends. The other player wins. - self.game_state = GameState(1 - self.current_player) - return - # TODO(): add the following checks - # - If player 0 made N repeatd back-and_forth moves in a row. - - def save_snapshot(self) -> None: - """Saves the current length of the effect history, and the current classical states of all pieces.""" - # Save the current length of the effect history. - self.effect_history_length.append(len(self.board.board.effect_history)) - # Save the classical states of all pieces. - snapshot = [] - for row in range(10): - for col in "abcdefghi": - piece = self.board.board[f"{col}{row}"] - snapshot.append( - [piece.type_.value, piece.color.value, piece.is_entangled] - ) - self.classical_properties_history.append(snapshot) - - def undo(self) -> None: - """Undo the last move, which includes reset quantum effects and classical properties.""" - if ( - len(self.effect_history_length) <= 1 - or len(self.classical_properties_history) <= 1 - ): - # length == 1 corresponds to the initial state, and no more undo could be made. - raise ValueError("Unable to undo any more.") - - # Recover the effects up to the last snapshot (which was done after the last move finished) by - # popping effects out of the effect history of the board until its length equals the last - # snapshot's length. - self.effect_history_length.pop() - last_length = self.effect_history_length[-1] - while len(self.board.board.effect_history) > last_length: - self.board.board.undo_last_effect() - - # Recover the classical properties of all pieces to the last snapshot. - self.classical_properties_history.pop() - snapshot = self.classical_properties_history[-1] - index = 0 - for row in range(10): - for col in "abcdefghi": - piece = self.board.board[f"{col}{row}"] - piece.type_ = Type(snapshot[index][0]) - piece.color = Color(snapshot[index][1]) - piece.is_entangled = snapshot[index][2] - index += 1 + print(e) + return False def play(self) -> None: """The loop where each player takes turn to play.""" - self.save_snapshot() while True: - move_success, output = self.next_move() + move_success = self.next_move() + print(self.board) if not move_success: # Continue if the player does not quit. if self.game_state == GameState.CONTINUES: - print(output) print("\nPlease re-enter your move.") continue - print(output) - if output != "Undoing.": - # Check if the game is over. - self.game_over() - # Update any empty or occupied pieces' classical state. - probs = self.update_board_by_sampling() - # Save the classical states and the current length of effect history. - self.save_snapshot() - print(self.board.to_str(probs)) + # Check if the game is over. + self.game_over() + # If the game continues, switch the player. if self.game_state == GameState.CONTINUES: - # If the game continues, switch the player. self.current_player = 1 - self.current_player self.board.current_player = self.current_player continue @@ -534,6 +166,23 @@ def play(self) -> None: print("Draw! Game is over.") break + def print_welcome(self) -> None: + """Prints the welcome message. Gets board language and players' name.""" + print(_WELCOME_MESSAGE) + print(_HELP_TEXT) + # TODO(): add whole set of Chinese interface support. + lang = input( + "Switch to Chinese board characters? (y/n) (default to be English) " + ) + if lang.lower() == "y": + self.lang = Language.ZH + else: + self.lang = Language.EN + name_0 = input("Player 0's name (default to be Player_0): ") + self.players_name.append("Player_0" if len(name_0) == 0 else name_0) + name_1 = input("Player 1's name (default to be Player_1): ") + self.players_name.append("Player_1" if len(name_1) == 0 else name_1) + def main(): game = QuantumChineseChess() diff --git a/unitary/examples/quantum_chinese_chess/chess_test.py b/unitary/examples/quantum_chinese_chess/chess_test.py index bd00ec65..01d9b70f 100644 --- a/unitary/examples/quantum_chinese_chess/chess_test.py +++ b/unitary/examples/quantum_chinese_chess/chess_test.py @@ -15,15 +15,7 @@ import io import sys from unitary.examples.quantum_chinese_chess.chess import QuantumChineseChess -from unitary.examples.quantum_chinese_chess.piece import Piece -from unitary.examples.quantum_chinese_chess.enums import ( - Language, - Color, - Type, - SquareState, - MoveType, - MoveVariant, -) +from unitary.examples.quantum_chinese_chess.enums import Language def test_game_init(monkeypatch): @@ -73,15 +65,9 @@ def test_apply_move_fail(monkeypatch): monkeypatch.setattr("builtins.input", lambda _: next(inputs)) game = QuantumChineseChess() with pytest.raises(ValueError, match="Could not move empty piece."): - game.apply_move("a8b8") + game.apply_move("a1b1") with pytest.raises(ValueError, match="Could not move the other player's piece."): - game.apply_move("a9b8") - with pytest.raises(ValueError, match="Two sources need to be the same type."): - game.apply_move("a0a3^a4") - with pytest.raises(ValueError, match="Two targets need to be the same type."): - game.apply_move("b2^a2h2") - with pytest.raises(ValueError, match="Two targets need to be the same color."): - game.apply_move("b2^b7h2") + game.apply_move("a0b1") def test_game_invalid_move(monkeypatch): @@ -96,263 +82,3 @@ def test_game_invalid_move(monkeypatch): in output.getvalue() ) sys.stdout = sys.__stdout__ - - -def test_check_classical_rule(monkeypatch): - output = io.StringIO() - sys.stdout = output - inputs = iter(["y", "Bob", "Ben"]) - monkeypatch.setattr("builtins.input", lambda _: next(inputs)) - game = QuantumChineseChess() - # The move is blocked by classical path piece. - with pytest.raises(ValueError, match="The path is blocked."): - game.check_classical_rule("a0", "a4", ["a3"]) - - # Target should not be a classical piece of the same color. - with pytest.raises( - ValueError, match="The target place has classical piece with the same color." - ): - game.check_classical_rule("a0", "a3", []) - - # ROOK - game.check_classical_rule("a0", "a2", []) - with pytest.raises(ValueError, match="ROOK cannot move like this."): - game.check_classical_rule("a0", "b1", []) - - # HORSE - game.check_classical_rule("b0", "c2", []) - with pytest.raises(ValueError, match="HORSE cannot move like this."): - game.check_classical_rule("b0", "c1", []) - - # ELEPHANT - game.check_classical_rule("c0", "e2", []) - with pytest.raises(ValueError, match="ELEPHANT cannot move like this."): - game.check_classical_rule("c0", "e1", []) - game.board.board["g5"].reset( - Piece("g5", SquareState.OCCUPIED, Type.ELEPHANT, Color.BLACK) - ) - with pytest.raises(ValueError, match="ELEPHANT cannot cross the river"): - game.check_classical_rule("g5", "i3", []) - game.board.board["c4"].reset( - Piece("c4", SquareState.OCCUPIED, Type.ELEPHANT, Color.RED) - ) - with pytest.raises(ValueError, match="ELEPHANT cannot cross the river"): - game.check_classical_rule("c4", "e6", []) - - # ADVISOR - game.check_classical_rule("d9", "e8", []) - with pytest.raises(ValueError, match="ADVISOR cannot move like this."): - game.check_classical_rule("d9", "d8", []) - with pytest.raises(ValueError, match="ADVISOR cannot leave the palace."): - game.check_classical_rule("d9", "c8", []) - with pytest.raises(ValueError, match="ADVISOR cannot leave the palace."): - game.check_classical_rule("f0", "g1", []) - - # KING - game.check_classical_rule("e9", "e8", []) - with pytest.raises(ValueError, match="KING cannot move like this."): - game.check_classical_rule("e9", "d8", []) - game.board.board["c0"].reset() - game.board.board["d0"].reset(game.board.board["e0"]) - game.board.board["e0"].reset() - with pytest.raises(ValueError, match="KING cannot leave the palace."): - game.check_classical_rule("d0", "c0", []) - - # CANNON - game.check_classical_rule("b7", "b4", []) - with pytest.raises(ValueError, match="CANNON cannot move like this."): - game.check_classical_rule("b7", "a8", []) - # Cannon could jump across exactly one piece. - game.check_classical_rule("b2", "b9", ["b7"]) - with pytest.raises(ValueError, match="CANNON cannot fire like this."): - game.check_classical_rule("b2", "b9", ["b5", "b7"]) - # Cannon cannot fire to a piece with same color. - game.board.board["b3"].reset(game.board.board["b2"]) - game.board.board["b2"].reset() - game.board.board["e3"].is_entangled = True - with pytest.raises( - ValueError, match="CANNON cannot fire to a piece with same color." - ): - game.check_classical_rule("b3", "e3", ["c3"]) - with pytest.raises(ValueError, match="CANNON cannot fire to an empty piece."): - game.check_classical_rule("b3", "d3", ["c3"]) - - # PAWN - game.check_classical_rule("a3", "a4", []) - with pytest.raises(ValueError, match="PAWN cannot move like this."): - game.check_classical_rule("a3", "a5", []) - with pytest.raises( - ValueError, match="PAWN can only go forward before crossing the river" - ): - game.check_classical_rule("e3", "f3", []) - with pytest.raises( - ValueError, match="PAWN can only go forward before crossing the river" - ): - game.check_classical_rule("g6", "h6", []) - with pytest.raises(ValueError, match="PAWN can not move backward."): - game.check_classical_rule("a3", "a2", []) - with pytest.raises(ValueError, match="PAWN can not move backward."): - game.check_classical_rule("g6", "g7", []) - # After crossing the rive the pawn could move horizontally. - game.board.board["c4"].reset(game.board.board["c6"]) - game.board.board["c6"].reset() - game.check_classical_rule("c4", "b4", []) - game.check_classical_rule("c4", "d4", []) - - -def test_classify_move_fail(monkeypatch): - output = io.StringIO() - sys.stdout = output - inputs = iter(["y", "Bob", "Ben"]) - monkeypatch.setattr("builtins.input", lambda _: next(inputs)) - game = QuantumChineseChess() - with pytest.raises( - ValueError, match="CANNON could not fire/capture without a cannon platform." - ): - game.classify_move(["b7"], ["b2"], [], [], [], []) - - with pytest.raises( - ValueError, match="Both sources need to be in quantum state in order to merge." - ): - game.classify_move(["b2", "h2"], ["e2"], [], [], [], []) - - game.board.board["c0"].reset(game.board.board["b7"]) - game.board.board["c0"].is_entangled = True - game.board.board["b7"].reset() - game.board.board["g0"].reset(game.board.board["h7"]) - game.board.board["g0"].is_entangled = True - game.board.board["h7"].reset() - with pytest.raises(ValueError, match="Currently CANNON cannot merge while firing."): - game.classify_move(["c0", "g0"], ["e0"], ["d0"], [], ["f0"], []) - - game.board.board["b3"].reset(game.board.board["b2"]) - game.board.board["b3"].is_entangled = True - game.board.board["b2"].reset() - game.board.board["d3"].reset(game.board.board["h2"]) - game.board.board["d3"].is_entangled = True - game.board.board["h2"].reset() - with pytest.raises( - ValueError, match="Currently we could only merge into an empty piece." - ): - game.classify_move(["b3", "d3"], ["c3"], [], [], [], []) - - with pytest.raises(ValueError, match="Currently CANNON cannot split while firing."): - game.classify_move(["g0"], ["e0", "i0"], ["f0"], [], ["h0"], []) - - game.board.board["d0"].is_entangled = True - with pytest.raises( - ValueError, match="Currently we could only split into empty pieces." - ): - game.classify_move(["d3"], ["d0", "d4"], [], [], [], []) - - game.board.board["d0"].reset() - game.board.board["f0"].reset() - with pytest.raises(ValueError, match="King split is not supported currently."): - game.classify_move(["e0"], ["d0", "f0"], [], [], [], []) - - -def test_classify_move_success(monkeypatch): - output = io.StringIO() - sys.stdout = output - inputs = iter(["y", "Bob", "Ben"]) - monkeypatch.setattr("builtins.input", lambda _: next(inputs)) - game = QuantumChineseChess() - # classical - assert game.classify_move(["h9"], ["g7"], [], [], [], []) == ( - MoveType.CLASSICAL, - MoveVariant.CLASSICAL, - ) - assert game.classify_move(["b2"], ["b9"], ["b7"], [], [], []) == ( - MoveType.CLASSICAL, - MoveVariant.CLASSICAL, - ) - - # jump basic - game.board.board["c9"].is_entangled = True - assert game.classify_move(["c9"], ["e7"], [], [], [], []) == ( - MoveType.JUMP, - MoveVariant.BASIC, - ) - game.board.board["b2"].is_entangled = True - assert game.classify_move(["b2"], ["e2"], [], [], [], []) == ( - MoveType.JUMP, - MoveVariant.BASIC, - ) - - # jump excluded - game.board.board["a3"].is_entangled = True - assert game.classify_move(["a0"], ["a3"], [], [], [], []) == ( - MoveType.JUMP, - MoveVariant.EXCLUDED, - ) - - # jump capture - game.board.board["g4"].reset(game.board.board["g6"]) - game.board.board["g4"].is_entangled = True - game.board.board["g6"].reset() - assert game.classify_move(["g4"], ["g3"], [], [], [], []) == ( - MoveType.JUMP, - MoveVariant.CAPTURE, - ) - - # slide basic - assert game.classify_move(["a0"], ["a4"], [], ["a3"], [], []) == ( - MoveType.SLIDE, - MoveVariant.BASIC, - ) - - # slide excluded - game.board.board["i7"].reset(game.board.board["h7"]) - game.board.board["i7"].is_entangled = True - game.board.board["h7"].reset() - game.board.board["i6"].is_entangled = True - assert game.classify_move(["i9"], ["i6"], [], ["i7"], [], []) == ( - MoveType.SLIDE, - MoveVariant.EXCLUDED, - ) - - # slide capture - assert game.classify_move(["a0"], ["a6"], [], ["a3"], [], []) == ( - MoveType.SLIDE, - MoveVariant.CAPTURE, - ) - - # split_jump basic - assert game.classify_move(["g4"], ["f4", "h4"], [], [], [], []) == ( - MoveType.SPLIT_JUMP, - MoveVariant.BASIC, - ) - - # split_slide basic - game.board.board["d3"].reset(game.board.board["h2"]) - game.board.board["h2"].reset() - game.board.board["c3"].is_entangled = True - game.board.board["e3"].is_entangled = True - assert game.classify_move(["d3"], ["b3", "f3"], [], ["c3"], [], ["e3"]) == ( - MoveType.SPLIT_SLIDE, - MoveVariant.BASIC, - ) - - # merge_jump basic - game.board.board["b7"].is_entangled = True - assert game.classify_move(["b7", "i7"], ["e7"], [], [], [], []) == ( - MoveType.MERGE_JUMP, - MoveVariant.BASIC, - ) - - # merge_slide basic - assert game.classify_move(["b7", "i7"], ["a7"], [], [], [], ["b7"]) == ( - MoveType.MERGE_SLIDE, - MoveVariant.BASIC, - ) - - # cannon_fire capture - assert game.classify_move(["i7"], ["i3"], [], ["i6"], [], []) == ( - MoveType.CANNON_FIRE, - MoveVariant.CAPTURE, - ) - game.board.board["i6"].is_entangled = False - assert game.classify_move(["i7"], ["i3"], ["i6"], [], [], []) == ( - MoveType.CANNON_FIRE, - MoveVariant.CAPTURE, - ) diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index 0ebffeb2..6fbf0f13 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -40,32 +40,32 @@ class GameState(enum.Enum): DRAW = 2 -# TODO(): consider if we could allow split/merge + excluded/capture, -# and cannon_split_fire/cannon_merge_fire + capture class MoveType(enum.Enum): """Each valid move will be classfied into one of the following MoveTypes.""" - UNSPECIFIED = 0 - CLASSICAL = 1 + NULL_TYPE = 0 + UNSPECIFIED_STANDARD = 1 JUMP = 2 SLIDE = 3 SPLIT_JUMP = 4 SPLIT_SLIDE = 5 MERGE_JUMP = 6 MERGE_SLIDE = 7 - CANNON_FIRE = 8 + HORSE_MOVE = 8 + HORSE_SPLIT_MOVE = 9 + HORSE_MERGE_MOVE = 10 + CANNON_FIRE = 11 class MoveVariant(enum.Enum): - """Each valid move will be classfied into one of the following MoveVariant, in addition to + """Each valid move will be classfied into one of the following MoveVariat, in addition to the MoveType above. """ UNSPECIFIED = 0 - CLASSICAL = 1 # Used together with MoveType = CLASSICAL. - BASIC = 2 # The target piece is empty. - EXCLUDED = 3 # The target piece has the same color. - CAPTURE = 4 # The target piece has the opposite color. + BASIC = 1 + EXCLUDED = 2 + CAPTURE = 3 class Color(enum.Enum): @@ -104,14 +104,14 @@ def type_of(c: str) -> Optional["Type"]: "e": Type.ELEPHANT, "a": Type.ADVISOR, "k": Type.KING, - "・": Type.EMPTY, + ".": Type.EMPTY, }.get(c.lower(), None) @staticmethod def symbol(type_: "Type", color: Color, lang: Language = Language.EN) -> str: """Returns symbol of the given piece according to its color and desired language.""" if type_ == Type.EMPTY: - return type_.value[0] + return "." if lang == Language.EN: # Return English symbols if color == Color.RED: return type_.value[0] From 9b9d8fb1476accbaf31fa3501a9f98ed46c3de49 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 10:31:59 -0700 Subject: [PATCH 20/31] update --- unitary/alpha/qubit_effects.py | 2 -- unitary/examples/quantum_chinese_chess/__init__.py | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/unitary/alpha/qubit_effects.py b/unitary/alpha/qubit_effects.py index 8f0b7810..9a659a18 100644 --- a/unitary/alpha/qubit_effects.py +++ b/unitary/alpha/qubit_effects.py @@ -207,11 +207,9 @@ def num_objects(self): return 3 def effect(self, *objects): - print("## PhasedSplit") yield cirq.ISWAP(objects[0].qubit, objects[1].qubit) ** 0.5 yield cirq.ISWAP(objects[0].qubit, objects[2].qubit) ** 0.5 yield cirq.ISWAP(objects[0].qubit, objects[2].qubit) ** 0.5 - print("## PhasedSplit 1") def __eq__(self, other): return isinstance(other, PhasedSplit) or NotImplemented diff --git a/unitary/examples/quantum_chinese_chess/__init__.py b/unitary/examples/quantum_chinese_chess/__init__.py index 64dd7178..4d904923 100644 --- a/unitary/examples/quantum_chinese_chess/__init__.py +++ b/unitary/examples/quantum_chinese_chess/__init__.py @@ -1,14 +1,15 @@ -# Copyright 2022 Google +# Copyright 2023 The Unitary Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# + +"""Package for the quantum Chinese chess game.""" From 505a28864a7a2fcc2df618b16ea2d467d7459805 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 11:18:50 -0700 Subject: [PATCH 21/31] update --- .../quantum_chinese_chess/enums_test.py | 10 ++--- .../examples/quantum_chinese_chess/piece1.py | 44 ------------------- 2 files changed, 5 insertions(+), 49 deletions(-) delete mode 100644 unitary/examples/quantum_chinese_chess/piece1.py diff --git a/unitary/examples/quantum_chinese_chess/enums_test.py b/unitary/examples/quantum_chinese_chess/enums_test.py index 7fa8cb2e..894a4991 100644 --- a/unitary/examples/quantum_chinese_chess/enums_test.py +++ b/unitary/examples/quantum_chinese_chess/enums_test.py @@ -19,7 +19,7 @@ def test_type_of(): assert Type.type_of("P") == Type.PAWN assert Type.type_of("k") == Type.KING assert Type.type_of("K") == Type.KING - assert Type.type_of("・") == Type.EMPTY + assert Type.type_of(".") == Type.EMPTY assert Type.type_of("b") == None @@ -34,7 +34,7 @@ def test_symbol(): assert Type.symbol(Type.HORSE, Color.RED, Language.ZH) == "马" assert Type.symbol(Type.HORSE, Color.BLACK, Language.ZH) == "馬" - assert Type.symbol(Type.EMPTY, Color.RED) == "・" - assert Type.symbol(Type.EMPTY, Color.BLACK) == "・" - assert Type.symbol(Type.EMPTY, Color.RED, Language.ZH) == "・" - assert Type.symbol(Type.EMPTY, Color.BLACK, Language.ZH) == "・" + assert Type.symbol(Type.EMPTY, Color.RED) == "." + assert Type.symbol(Type.EMPTY, Color.BLACK) == "." + assert Type.symbol(Type.EMPTY, Color.RED, Language.ZH) == "." + assert Type.symbol(Type.EMPTY, Color.BLACK, Language.ZH) == "." diff --git a/unitary/examples/quantum_chinese_chess/piece1.py b/unitary/examples/quantum_chinese_chess/piece1.py deleted file mode 100644 index 130afab2..00000000 --- a/unitary/examples/quantum_chinese_chess/piece1.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2023 The Unitary Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from unitary.alpha import QuantumObject -from unitary.examples.quantum_chinese_chess.enums import ( - SquareState, - Language, - Color, - Type, -) - - -class Piece: - def __init__(self, type_: Type, color: Color): - self.type_ = type_ - self.color = color - self.is_entangled = False - - def symbol(self, lang: Language = Language.EN) -> str: - return Type.symbol(self.type_, self.color, lang) - - def __str__(self): - return self.symbol() - - def clear(self): - self.type_ = Type.EMPTY - self.color = Color.NA - self.is_entangled = False - - -class QuantumPiece(Piece, QuantumObject): - def __init__(self, name: str, state: SquareState, type_: Type, color: Color): - QuantumObject.__init__(self, name, state) - Piece.__init__(self, name, state, type_, color) From f8842be177daf23b85e4a627554676ac353888b8 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 11:20:41 -0700 Subject: [PATCH 22/31] update --- .../quantum_chinese_chess/piece_test.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/piece_test.py b/unitary/examples/quantum_chinese_chess/piece_test.py index e4a70cb3..6141ff0a 100644 --- a/unitary/examples/quantum_chinese_chess/piece_test.py +++ b/unitary/examples/quantum_chinese_chess/piece_test.py @@ -34,9 +34,9 @@ def test_symbol(): assert p1.symbol(Language.ZH) == "馬" p2 = Piece("c2", SquareState.EMPTY, Type.EMPTY, Color.NA) - assert p2.symbol() == "・" - assert p2.__str__() == "・" - assert p2.symbol(Language.ZH) == "・" + assert p2.symbol() == "." + assert p2.__str__() == "." + assert p2.symbol(Language.ZH) == "." def test_enum(): @@ -47,16 +47,3 @@ def test_enum(): assert board.peek() == [ [SquareState.OCCUPIED, SquareState.OCCUPIED, SquareState.EMPTY] ] - - -def test_reset(): - p0 = Piece("a0", SquareState.OCCUPIED, Type.CANNON, Color.RED) - p1 = Piece("b1", SquareState.OCCUPIED, Type.HORSE, Color.BLACK) - - p0.reset() - assert p0.type_ == Type.EMPTY - assert p0.color == Color.NA - - p0.reset(p1) - assert p0.type_ == p1.type_ - assert p0.color == p1.color From 6ee0c94a1b850fe5c897473b1d546d0edcea4f5c Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 11:24:27 -0700 Subject: [PATCH 23/31] up --- .../quantum_chinese_chess/piece_test.py | 16 + .../quantum_chinese_chess/quantum_world.py | 519 ------------------ 2 files changed, 16 insertions(+), 519 deletions(-) delete mode 100644 unitary/examples/quantum_chinese_chess/quantum_world.py diff --git a/unitary/examples/quantum_chinese_chess/piece_test.py b/unitary/examples/quantum_chinese_chess/piece_test.py index 6141ff0a..291a2691 100644 --- a/unitary/examples/quantum_chinese_chess/piece_test.py +++ b/unitary/examples/quantum_chinese_chess/piece_test.py @@ -47,3 +47,19 @@ def test_enum(): assert board.peek() == [ [SquareState.OCCUPIED, SquareState.OCCUPIED, SquareState.EMPTY] ] + + +def test_reset(): + p0 = Piece("a0", SquareState.OCCUPIED, Type.CANNON, Color.RED) + p1 = Piece("b1", SquareState.OCCUPIED, Type.HORSE, Color.BLACK) + p1.is_entangled = True + + p0.reset() + assert p0.type_ == Type.EMPTY + assert p0.color == Color.NA + assert p0.is_entangled == False + + p0.reset(p1) + assert p0.type_ == p1.type_ + assert p0.color == p1.color + assert p0.is_entangled == p1.is_entangled diff --git a/unitary/examples/quantum_chinese_chess/quantum_world.py b/unitary/examples/quantum_chinese_chess/quantum_world.py deleted file mode 100644 index 7d6b3340..00000000 --- a/unitary/examples/quantum_chinese_chess/quantum_world.py +++ /dev/null @@ -1,519 +0,0 @@ -# Copyright 2023 The Unitary Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import copy -import enum -from typing import cast, Dict, Iterable, List, Optional, Sequence, Set, Tuple, 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: - """A collection of `QuantumObject`s with effects. - - This object represents an entire state of a quantum game. - This includes all the `QuantumObjects` as well as the effects - that have been applied to them. It also includes all - criteria of measurement so that repetitions of the circuit - will be the same even if some of the quantum objects have - already been measured, - - This object also has a history so that effects can be undone. - - 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, - sampler=cirq.Simulator(), - compile_to_qubits: bool = False, - ): - self.clear() - self.sampler = sampler - self.use_sparse = isinstance(sampler, SparseSimulator) - self.compile_to_qubits = compile_to_qubits - - if isinstance(objects, QuantumObject): - objects = [objects] - if objects is not None: - for obj in objects: - self.add_object(obj) - - def clear(self) -> None: - """Removes all objects and effects from this QuantumWorld. - - This will reset the QuantumWorld to an empty state. - """ - self.circuit = cirq.Circuit() - self.effect_history: List[Tuple[cirq.Circuit, Dict[QuantumObject, int]]] = [] - self.object_name_dict: Dict[str, QuantumObject] = {} - self.ancilla_names: Set[str] = 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] = {} - - def copy(self) -> "QuantumWorld": - new_objects = [] - new_post_selection: Dict[QuantumObject, int] = {} - for obj in self.object_name_dict.values(): - new_obj = copy.copy(obj) - new_objects.append(new_obj) - if obj in self.post_selection: - new_post_selection[new_obj] = self.post_selection[obj] - new_world = self.__class__( - objects=new_objects, - sampler=self.sampler, - compile_to_qubits=self.compile_to_qubits, - ) - new_world.circuit = self.circuit.copy() - new_world.ancilla_names = self.ancilla_names.copy() - new_world.effect_history = [ - (circuit.copy(), copy.copy(post_selection)) - for circuit, post_selection in self.effect_history - ] - new_world.post_selection = new_post_selection - return new_world - - def add_object(self, obj: QuantumObject): - """Adds a QuantumObject to the QuantumWorld. - - Raises: - ValueError: if an object with the same name has - already been added to the world. - """ - if obj.name in self.object_name_dict: - 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: - 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 - def objects(self) -> List[QuantumObject]: - return list(self.object_name_dict.values()) - - @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 - ] - - def get_object_by_name(self, name: str) -> Optional[QuantumObject]: - """Returns the object with the given name. - - If the object with that name does not exist in this QuantumWorld, - the function returns None. - """ - return self.object_name_dict.get(name) - - def combine_with(self, other_world: "QuantumWorld"): - """Combines all the objects from the specified world into this one. - - This will add all the objects as well as all the effects into the - current world. The passed in world then becomes unusable. - - Note that the effect history is cleared when this function is called, - so previous effects cannot be undone. - """ - my_keys = set(self.object_name_dict.keys()) - other_keys = set(other_world.object_name_dict.keys()) - if my_keys.intersection(other_keys): - raise ValueError("Cannot combine two worlds with overlapping object keys") - if self.use_sparse != other_world.use_sparse: - raise ValueError("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 - self.effect_history.clear() - # 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. - - 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 - or isinstance(op, PostSelectOperation) - or op.gate is cirq.X - ): - strategy = cirq.InsertStrategy.EARLIEST - else: - strategy = cirq.InsertStrategy.NEW - - if self.compile_to_qubits: - op = self._compile_op(op) - - self.circuit.append(op, strategy=strategy) - - 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(#77): 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 = qid_shape[0] - if qudit_dim == 2: - return op - num_qudits = len(qid_shape) - compiled_qubits = [] - 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, - 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.""" - self.effect_history.append( - (self.circuit.copy(), copy.copy(self.post_selection)) - ) - for op in op_list: - self._append_op(op) - - def undo_last_effect(self): - """Restores the `QuantumWorld` to the state before the last effect. - - Note that pop() is considered to be an effect for the purposes - of this call. - - Raises: - IndexError if there are no effects in the history. - """ - if not self.effect_history: - raise IndexError("No effects to undo") - self.circuit, self.post_selection = self.effect_history.pop() - - def _suggest_num_reps(self, sample_size: int) -> int: - """Guess the number of raw samples needed to get sample_size results. - Assume that each post-selection is about 50/50. - Noise and error mitigation will discard reps, so increase the total - number of repetitions to compensate. - """ - if self.use_sparse: - return sample_size - if len(self.post_selection) >= 1: - sample_size <<= len(self.post_selection) + 1 - if sample_size < 100: - sample_size = 100 - return sample_size - - def _interpret_result(self, result: Union[int, Iterable[int]]) -> 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. - return cirq.big_endian_bits_to_int(result) - if isinstance(result, Iterable): - # If it is a single-element iterable, return the first element. - result_list = list(result) - if len(result_list) != 1: - raise ValueError( - f"Cannot interpret a multivalued iterable {result} as a " - "single result for a non-compiled world." - ) - return result_list[0] - return result - - def force_measurement( - self, obj: QuantumObject, result: Union[enum.Enum, int] - ) -> None: - """Measures a QuantumObject with a defined outcome. - - This function will move the qubit to an ancilla and set - a post-selection criteria on it in order to force it - 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) - # 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.update( - {*zip(obj_qubits, new_obj_qubits), *zip(new_obj_qubits, obj_qubits)} - ) - - self.circuit = self.circuit.transform_qubits( - 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 - if self.use_sparse: - self._append_op(PostSelectOperation(new_obj.qubit, post_selection)) - - def peek( - self, - objects: Optional[Sequence[Union[QuantumObject, str]]] = None, - count: int = 1, - convert_to_enum: bool = True, - _existing_list: Optional[List[List[Union[enum.Enum, int]]]] = None, - _num_reps: Optional[int] = None, - ) -> List[List[Union[enum.Enum, int]]]: - """Measures the state of the system 'non-destructively'. - - This function will measure the state of each object. - It will _not_ modify the circuit of the QuantumWorld. - - Returns: - A list of measurement results. The length of the list will be - equal to the count parameter. Each element will be a list - of measurement results for each object. - """ - if _num_reps is None: - num_reps = self._suggest_num_reps(count) - else: - if _num_reps > 1e6: - raise RecursionError( - f"Count {count} reached without sufficient results. " - "Likely post-selection error" - ) - num_reps = _num_reps - - measure_circuit = self.circuit.copy() - if objects is None: - quantum_objects = self.public_objects - else: - quantum_objects = [ - self[obj_or_str] if isinstance(obj_or_str, str) else obj_or_str - for obj_or_str in objects - ] - measure_set = set(quantum_objects) - measure_set.update(self.post_selection.keys()) - 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 - rtn_list = _existing_list or [] - for rep in range(num_reps): - post_selected = True - for obj in self.post_selection.keys(): - result = self._interpret_result(results.measurements[obj.name][rep]) - if result != self.post_selection[obj]: - post_selected = False - break - if post_selected: - # All post-selections are satisfied. - rtn_list.append( - [ - self._interpret_result(results.measurements[obj.name][rep]) - for obj in quantum_objects - ] - ) - if len(rtn_list) == count: - break - if len(rtn_list) < count: - # We post-selected too much, get more reps - return self.peek( - quantum_objects, - count, - convert_to_enum, - rtn_list, - _num_reps=num_reps * 10, - ) - - if convert_to_enum: - rtn_list = [ - [quantum_objects[idx].enum_type(meas) for idx, meas in enumerate(res)] - for res in rtn_list - ] - - return rtn_list - - def pop( - self, - objects: Optional[Sequence[Union[QuantumObject, str]]] = None, - convert_to_enum: bool = True, - ) -> List[Union[enum.Enum, int]]: - """Peek the given objects (or all public objects if none is supplied) once and force measure them - with the peeked result. - - Returns: - A list of measurement results with one for each object. - """ - self.effect_history.append( - (self.circuit.copy(), copy.copy(self.post_selection)) - ) - if objects is None: - quantum_objects = self.public_objects - else: - quantum_objects = [ - self[obj_or_str] if isinstance(obj_or_str, str) else obj_or_str - for obj_or_str in objects - ] - results = self.peek(quantum_objects, convert_to_enum=convert_to_enum) - for idx, result in enumerate(results[0]): - self.force_measurement(quantum_objects[idx], result) - - return results[0] - - def get_histogram( - self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 - ) -> List[Dict[int, int]]: - """Creates histogram based on measurements (peeks) carried out. - - Parameters: - objects: List of QuantumObjects - count: Number of measurements - - Returns: - A list with one element for each object. Each element contains a dictionary with - counts for each state of the given object. - """ - if not objects: - objects = self.public_objects - peek_results = self.peek(objects=objects, convert_to_enum=False, count=count) - histogram = [] - for obj in objects: - histogram.append({state: 0 for state in range(obj.num_states)}) - for result in peek_results: - for idx in range(len(objects)): - histogram[idx][cast(int, result[idx])] += 1 - return histogram - - def get_probabilities( - self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 - ) -> List[Dict[int, float]]: - """Calculates the probabilities based on measurements (peeks) carried out. - - Parameters: - objects: List of QuantumObjects - count: Number of measurements - - Returns: - A list with one element for each object. Each element contains a dictionary with - the probability for each state of the given object. - """ - histogram = self.get_histogram(objects=objects, count=count) - probabilities = [] - for obj_hist in histogram: - probabilities.append( - {state: obj_hist[state] / count for state in range(len(obj_hist))} - ) - return probabilities - - def get_binary_probabilities( - self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 100 - ) -> List[float]: - """Calculates the total probabilities for all non-zero states - based on measurements (peeks) carried out. - - Parameters: - objects: List of QuantumObjects - count: Number of measurements - - Returns: - A list with one element for each object which contains - the probability for the event state!=0. Which is the same as - 1.0-Probability(state==0). - """ - full_probs = self.get_probabilities(objects=objects, count=count) - binary_probs = [] - for one_probs in full_probs: - binary_probs.append(1 - one_probs[0]) - return binary_probs - - def __getitem__(self, name: str) -> QuantumObject: - quantum_object = self.object_name_dict.get(name, None) - if not quantum_object: - raise KeyError(f"{name} did not exist in this world.") - return quantum_object From fdee58593c2f7a4204e840e47875ed84c8000e5c Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 11:34:48 -0700 Subject: [PATCH 24/31] update --- .../examples/quantum_chinese_chess/board.py | 71 +------ .../examples/quantum_chinese_chess/piece.py | 19 -- .../quantum_chinese_chess/piece_test.py | 16 -- .../quantum_chinese_chess/test_utils.py | 188 +----------------- 4 files changed, 10 insertions(+), 284 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index c2401315..bcbe83b5 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np -from typing import List, Tuple +from typing import List import unitary.alpha as alpha from unitary.examples.quantum_chinese_chess.enums import ( SquareState, @@ -35,7 +34,6 @@ def __init__( ): self.board = board self.current_player = current_player - # This saves the locations of KINGs in the order of [RED_KING_LOCATION, BLACK_KING_LOCATION]. self.king_locations = king_locations self.lang = Language.EN # The default language is English. @@ -78,11 +76,6 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": board = alpha.QuantumWorld(chess_board.values()) # Here 0 means the player RED while 1 the player BLACK. current_player = 0 if "w" in turns else 1 - # TODO(): maybe add check to make sure the input fen itself is correct. - if len(king_locations) != 2: - raise ValueError( - f"We expect two KINGs on the board, but got {len(king_locations)}." - ) return cls(board, current_player, king_locations) def __str__(self): @@ -126,65 +119,3 @@ def __str__(self): .replace("abcdefghi", " abcdefghi") .translate(translation) ) - - def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: - """Returns the nonempty classical and quantum pieces from source to target (excluded).""" - x0 = ord(source[0]) - x1 = ord(target[0]) - dx = x1 - x0 - y0 = int(source[1]) - y1 = int(target[1]) - dy = y1 - y0 - # In case of only moving one step, return empty path pieces. - if abs(dx) + abs(dy) <= 1: - return [], [] - # In case of advisor moving, return empty path pieces. - # TODO(): maybe move this to the advisor move check. - if abs(dx) == 1 and abs(dy) == 1: - return [], [] - pieces = [] - classical_pieces = [] - quantum_pieces = [] - dx_sign = np.sign(dx) - dy_sign = np.sign(dy) - # In case of elephant move, there should only be one path piece. - if abs(dx) == abs(dy): - pieces.append(f"{chr(x0 + dx_sign)}{y0 + dy_sign}") - # This could be move of rook, king, pawn or cannon. - elif dx == 0: - for i in range(1, abs(dy)): - pieces.append(f"{chr(x0)}{y0 + dy_sign * i}") - # This could be move of rook, king, pawn or cannon. - elif dy == 0: - for i in range(1, abs(dx)): - pieces.append(f"{chr(x0 + dx_sign * i)}{y0}") - # This covers four possible directions of horse move. - elif abs(dx) == 2 and abs(dy) == 1: - pieces.append(f"{chr(x0 + dx_sign)}{y0}") - # This covers the other four possible directions of horse move. - elif abs(dy) == 2 and abs(dx) == 1: - pieces.append(f"{chr(x0)}{y0 + dy_sign}") - else: - raise ValueError("Unexpected input to path_pieces().") - for piece in pieces: - if self.board[piece].is_entangled: - quantum_pieces.append(piece) - elif self.board[piece].type_ != Type.EMPTY: - classical_pieces.append(piece) - return classical_pieces, quantum_pieces - - def flying_general_check(self) -> bool: - """Check and return if the two KINGs are directly facing each other (i.e. in the same column) without any pieces in between.""" - king_0 = self.king_locations[0] - king_1 = self.king_locations[1] - if king_0[0] != king_1[0]: - # If they are in different columns, the check fails. Game continues. - return False - classical_pieces, quantum_pieces = self.path_pieces(king_0, king_1) - if len(classical_pieces) > 0: - # If there are classical pieces between two KINGs, the check fails. Game continues. - return False - if len(quantum_pieces) == 0: - # If there are no pieces between two KINGs, the check successes. Game ends. - return True - # TODO(): add check when there are quantum pieces in between. diff --git a/unitary/examples/quantum_chinese_chess/piece.py b/unitary/examples/quantum_chinese_chess/piece.py index 628ef959..6335c530 100644 --- a/unitary/examples/quantum_chinese_chess/piece.py +++ b/unitary/examples/quantum_chinese_chess/piece.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional from unitary.alpha import QuantumObject from unitary.examples.quantum_chinese_chess.enums import ( SquareState, @@ -26,27 +25,9 @@ def __init__(self, name: str, state: SquareState, type_: Type, color: Color): QuantumObject.__init__(self, name, state) self.type_ = type_ self.color = color - # TODO(): maybe modify QuantumObject to allow it to: - # - not create qubit at initialization; - # - add qubit and set it to the current classical state when needed. - self.is_entangled = False def symbol(self, lang: Language = Language.EN) -> str: - """Returns the symbol of this piece according to its type, color and language.""" return Type.symbol(self.type_, self.color, lang) def __str__(self): return self.symbol() - - def reset(self, piece: "Piece" = None) -> None: - """Modifies the classical attributes of the piece. - If piece is provided, then its type_ and color is copied, otherwise set the current piece to be an empty piece. - """ - if piece is not None: - self.type_ = piece.type_ - self.color = piece.color - self.is_entangled = piece.is_entangled - else: - self.type_ = Type.EMPTY - self.color = Color.NA - self.is_entangled = False diff --git a/unitary/examples/quantum_chinese_chess/piece_test.py b/unitary/examples/quantum_chinese_chess/piece_test.py index 291a2691..6141ff0a 100644 --- a/unitary/examples/quantum_chinese_chess/piece_test.py +++ b/unitary/examples/quantum_chinese_chess/piece_test.py @@ -47,19 +47,3 @@ def test_enum(): assert board.peek() == [ [SquareState.OCCUPIED, SquareState.OCCUPIED, SquareState.EMPTY] ] - - -def test_reset(): - p0 = Piece("a0", SquareState.OCCUPIED, Type.CANNON, Color.RED) - p1 = Piece("b1", SquareState.OCCUPIED, Type.HORSE, Color.BLACK) - p1.is_entangled = True - - p0.reset() - assert p0.type_ == Type.EMPTY - assert p0.color == Color.NA - assert p0.is_entangled == False - - p0.reset(p1) - assert p0.type_ == p1.type_ - assert p0.color == p1.color - assert p0.is_entangled == p1.is_entangled diff --git a/unitary/examples/quantum_chinese_chess/test_utils.py b/unitary/examples/quantum_chinese_chess/test_utils.py index 8e0f82e8..98184187 100644 --- a/unitary/examples/quantum_chinese_chess/test_utils.py +++ b/unitary/examples/quantum_chinese_chess/test_utils.py @@ -12,184 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. from unitary.alpha import QuantumObject, QuantumWorld -from unitary.examples.quantum_chinese_chess.enums import SquareState, Type, Color -from unitary.examples.quantum_chinese_chess.board import Board -from unitary.examples.quantum_chinese_chess.piece import Piece -from unitary import alpha -from typing import List, Dict -from collections import defaultdict -from scipy.stats import chisquare +from unitary.examples.quantum_chinese_chess.enums import SquareState +from string import ascii_lowercase, digits -_EMPTY_FEN = "9/9/9/9/9/9/9/9/9/9 w---1" - - -def set_board(positions: List[str]) -> Board: - """Returns a board with the specified positions filled with - RED ROOKs. - """ - board = Board.from_fen(_EMPTY_FEN) - for position in positions: - board.board[position].reset( - Piece(position, SquareState.OCCUPIED, Type.ROOK, Color.RED) - ) - alpha.Flip()(board.board[position]) - return board - - -def location_to_bit(location: str) -> int: - """Transform location notation (e.g. "a3") into a bitboard bit number. - The return value ranges from 0 to 89. - """ - x = ord(location[0]) - ord("a") - y = int(location[1]) - return y * 9 + x - - -def locations_to_bitboard(locations: List[str]) -> int: - """Transform a list of locations into a 90-bit board bitstring. - Each nonzero bit of the bitstring indicates that the corresponding - piece is occupied. - """ - bitboard = 0 - for location in locations: - bitboard += 1 << location_to_bit(location) - return bitboard - - -def nth_bit_of(n: int, bit_board: int) -> bool: - """Returns the `n`-th (zero-based) bit of a 90-bit bitstring `bit_board`.""" - return (bit_board >> n) % 2 == 1 - - -def bit_to_location(bit: int) -> str: - """Transform a bitboard bit number into a location (e.g. "a3").""" - y = bit // 9 - x = chr(bit % 9 + ord("a")) - return f"{x}{y}" - - -def bitboard_to_locations(bitboard: int) -> List[str]: - """Transform a 90-bit bitstring `bitboard` into a list of locations.""" - locations = [] - for n in range(90): - if nth_bit_of(n, bitboard): - locations.append(bit_to_location(n)) - return locations - - -def sample_board(board: Board, repetitions: int) -> List[int]: - """Sample the given `board` by the given `repetitions`. - Returns a list of 90-bit bitstring, each corresponding to one sample. - """ - samples = board.board.peek(count=repetitions, convert_to_enum=False) - # Convert peek results (in List[List[int]]) into List[int]. - samples = [ - int("0b" + "".join([str(i) for i in sample[::-1]]), base=2) - for sample in samples - ] - return samples - - -def print_samples(samples: List[int]) -> None: - """Aggregate all the samples and print the dictionary of {locations: count}.""" - sample_dict = {} - for sample in samples: - if sample not in sample_dict: - sample_dict[sample] = 0 - sample_dict[sample] += 1 - print("Actual samples:") - for key in sample_dict: - print(f"{bitboard_to_locations(key)}: {sample_dict[key]}") - - -def get_board_probability_distribution( - board: Board, repetitions: int = 1000 -) -> Dict[int, float]: - """Returns the probability distribution for each board found in the sample. - The values are returned as a dict{bitboard(int): probability(float)}. - """ - board_probabilities: Dict[int, float] = {} - - samples = sample_board(board, repetitions) - for sample in samples: - if sample not in board_probabilities: - board_probabilities[sample] = 0.0 - board_probabilities[sample] += 1.0 - - for board in board_probabilities: - board_probabilities[board] /= repetitions - - return board_probabilities - - -def assert_samples_in(board: Board, probabilities: Dict[int, float]) -> None: - """Samples the given `board` and asserts that all samples are within - the given `probabilities` (i.e. a map from bitstring into its possibility), - and that each possibility is represented at least once in the samples. - """ - samples = sample_board(board, 500) - assert len(samples) == 500 - all_in = all(sample in probabilities for sample in samples) - assert all_in, print_samples(samples) - # Make sure each possibility is represented at least once. - for possibility in probabilities: - any_in = any(sample == possibility for sample in samples) - assert any_in, print_samples(samples) - - -def assert_sample_distribution( - board: Board, probabilities: Dict[int, float], p_significant: float = 1e-6 -) -> None: - """Performs a chi-squared test that samples follow an expected distribution. - `probabilities` is a map from bitboards to expected probability. An - AssertionError is raised if any of the samples is not in the map, or if the - expected versus observed samples fails the chi-squared test. - """ - n_samples = 500 - assert abs(sum(probabilities.values()) - 1) < 1e-9 - samples = sample_board(board, n_samples) - counts = defaultdict(int) - for sample in samples: - assert sample in probabilities, print_samples(samples) - counts[sample] += 1 - observed = [] - expected = [] - for position, probability in probabilities.items(): - observed.append(counts[position]) - expected.append(n_samples * probability) - p = chisquare(observed, expected).pvalue - assert ( - p > p_significant - ), f"Observed {observed} is far from expected {expected} (p = {p})" - - -def assert_this_or_that(samples: List[int], this: int, that: int) -> None: - """Asserts all the samples are either equal to `this` or `that`, - and that at least one of them exists in the samples. - """ - assert any(sample == this for sample in samples), print_samples(samples) - assert any(sample == that for sample in samples), print_samples(samples) - assert all(sample == this or sample == that for sample in samples), print_samples( - samples - ) - - -def assert_prob_about( - probabilities: Dict[int, float], that: int, expected: float, atol: float = 0.05 -) -> None: - """Checks that the probability of `that` is within `atol` of the value of `expected`.""" - assert that in probabilities, print_samples(list(probabilities.keys())) - assert probabilities[that] > expected - atol, print_samples( - list(probabilities.keys()) - ) - assert probabilities[that] < expected + atol, print_samples( - list(probabilities.keys()) - ) - - -def assert_fifty_fifty(probabilities, that): - """Checks that the probability of `that` is close to 50%.""" - assert_prob_about(probabilities, that, 0.5), print_samples( - list(probabilities.keys()) - ) +# Build quantum objects a0 to i9, and add them to a quantum world. +def init_board() -> QuantumWorld: + board = {} + for col in ascii_lowercase[:9]: + for row in digits: + board[col + row] = QuantumObject(col + row, SquareState.EMPTY) + return QuantumWorld(list(board.values())) From 82b249efbc24658b4e676b1495247eba84ad0c43 Mon Sep 17 00:00:00 2001 From: madcpf Date: Mon, 23 Oct 2023 11:39:27 -0700 Subject: [PATCH 25/31] up --- .../examples/quantum_chinese_chess/board.py | 71 ++++++++++++++++++- .../examples/quantum_chinese_chess/piece.py | 17 +++++ .../quantum_chinese_chess/piece_test.py | 13 ++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index bcbe83b5..c2401315 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import List +import numpy as np +from typing import List, Tuple import unitary.alpha as alpha from unitary.examples.quantum_chinese_chess.enums import ( SquareState, @@ -34,6 +35,7 @@ def __init__( ): self.board = board self.current_player = current_player + # This saves the locations of KINGs in the order of [RED_KING_LOCATION, BLACK_KING_LOCATION]. self.king_locations = king_locations self.lang = Language.EN # The default language is English. @@ -76,6 +78,11 @@ def from_fen(cls, fen: str = _INITIAL_FEN) -> "Board": board = alpha.QuantumWorld(chess_board.values()) # Here 0 means the player RED while 1 the player BLACK. current_player = 0 if "w" in turns else 1 + # TODO(): maybe add check to make sure the input fen itself is correct. + if len(king_locations) != 2: + raise ValueError( + f"We expect two KINGs on the board, but got {len(king_locations)}." + ) return cls(board, current_player, king_locations) def __str__(self): @@ -119,3 +126,65 @@ def __str__(self): .replace("abcdefghi", " abcdefghi") .translate(translation) ) + + def path_pieces(self, source: str, target: str) -> Tuple[List[str], List[str]]: + """Returns the nonempty classical and quantum pieces from source to target (excluded).""" + x0 = ord(source[0]) + x1 = ord(target[0]) + dx = x1 - x0 + y0 = int(source[1]) + y1 = int(target[1]) + dy = y1 - y0 + # In case of only moving one step, return empty path pieces. + if abs(dx) + abs(dy) <= 1: + return [], [] + # In case of advisor moving, return empty path pieces. + # TODO(): maybe move this to the advisor move check. + if abs(dx) == 1 and abs(dy) == 1: + return [], [] + pieces = [] + classical_pieces = [] + quantum_pieces = [] + dx_sign = np.sign(dx) + dy_sign = np.sign(dy) + # In case of elephant move, there should only be one path piece. + if abs(dx) == abs(dy): + pieces.append(f"{chr(x0 + dx_sign)}{y0 + dy_sign}") + # This could be move of rook, king, pawn or cannon. + elif dx == 0: + for i in range(1, abs(dy)): + pieces.append(f"{chr(x0)}{y0 + dy_sign * i}") + # This could be move of rook, king, pawn or cannon. + elif dy == 0: + for i in range(1, abs(dx)): + pieces.append(f"{chr(x0 + dx_sign * i)}{y0}") + # This covers four possible directions of horse move. + elif abs(dx) == 2 and abs(dy) == 1: + pieces.append(f"{chr(x0 + dx_sign)}{y0}") + # This covers the other four possible directions of horse move. + elif abs(dy) == 2 and abs(dx) == 1: + pieces.append(f"{chr(x0)}{y0 + dy_sign}") + else: + raise ValueError("Unexpected input to path_pieces().") + for piece in pieces: + if self.board[piece].is_entangled: + quantum_pieces.append(piece) + elif self.board[piece].type_ != Type.EMPTY: + classical_pieces.append(piece) + return classical_pieces, quantum_pieces + + def flying_general_check(self) -> bool: + """Check and return if the two KINGs are directly facing each other (i.e. in the same column) without any pieces in between.""" + king_0 = self.king_locations[0] + king_1 = self.king_locations[1] + if king_0[0] != king_1[0]: + # If they are in different columns, the check fails. Game continues. + return False + classical_pieces, quantum_pieces = self.path_pieces(king_0, king_1) + if len(classical_pieces) > 0: + # If there are classical pieces between two KINGs, the check fails. Game continues. + return False + if len(quantum_pieces) == 0: + # If there are no pieces between two KINGs, the check successes. Game ends. + return True + # TODO(): add check when there are quantum pieces in between. diff --git a/unitary/examples/quantum_chinese_chess/piece.py b/unitary/examples/quantum_chinese_chess/piece.py index 6335c530..e8d4b79a 100644 --- a/unitary/examples/quantum_chinese_chess/piece.py +++ b/unitary/examples/quantum_chinese_chess/piece.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from unitary.alpha import QuantumObject from unitary.examples.quantum_chinese_chess.enums import ( SquareState, @@ -25,9 +26,25 @@ def __init__(self, name: str, state: SquareState, type_: Type, color: Color): QuantumObject.__init__(self, name, state) self.type_ = type_ self.color = color + # TODO(): maybe modify QuantumObject to allow it to: + # - not create qubit at initialization; + # - add qubit and set it to the current classical state when needed. + self.is_entangled = False def symbol(self, lang: Language = Language.EN) -> str: + """Returns the symbol of this piece according to its type, color and language.""" return Type.symbol(self.type_, self.color, lang) def __str__(self): return self.symbol() + + def reset(self, piece: "Piece" = None) -> None: + """Modifies the classical attributes of the piece. + If piece is provided, then its type_ and color is copied, otherwise set the current piece to be an empty piece. + """ + if piece is not None: + self.type_ = piece.type_ + self.color = piece.color + else: + self.type_ = Type.EMPTY + self.color = Color.NA diff --git a/unitary/examples/quantum_chinese_chess/piece_test.py b/unitary/examples/quantum_chinese_chess/piece_test.py index 6141ff0a..c15ac942 100644 --- a/unitary/examples/quantum_chinese_chess/piece_test.py +++ b/unitary/examples/quantum_chinese_chess/piece_test.py @@ -47,3 +47,16 @@ def test_enum(): assert board.peek() == [ [SquareState.OCCUPIED, SquareState.OCCUPIED, SquareState.EMPTY] ] + + +def test_reset(): + p0 = Piece("a0", SquareState.OCCUPIED, Type.CANNON, Color.RED) + p1 = Piece("b1", SquareState.OCCUPIED, Type.HORSE, Color.BLACK) + + p0.reset() + assert p0.type_ == Type.EMPTY + assert p0.color == Color.NA + + p0.reset(p1) + assert p0.type_ == p1.type_ + assert p0.color == p1.color From a4ed1940843aaca42da5b19d0c2742a9f87c1432 Mon Sep 17 00:00:00 2001 From: madcpf Date: Wed, 25 Oct 2023 21:47:08 -0700 Subject: [PATCH 26/31] update --- .../examples/quantum_chinese_chess/move_test.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index a81510a2..23704685 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -144,7 +144,7 @@ def test_to_str(): assert move3.to_str(3) == "a0a6:SLIDE:CAPTURE:RED_ROOK->BLACK_PAWN" -def test_split_jump(): +def test_split_jump_classical_source(): # Source is in classical state. board = set_board(["a1"]) world = board.board @@ -154,6 +154,8 @@ def test_split_jump(): assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + +def test_split_jump_quantum_source(): # Source is in quantum state. board = set_board(["a1"]) world = board.board @@ -169,7 +171,7 @@ def test_split_jump(): ) -def test_merge_jump(): +def test_merge_jump_perfect_merge(): # Two quantum pieces split from one source could be merge back to one. board = set_board(["a1"]) world = board.board @@ -177,6 +179,8 @@ def test_merge_jump(): MergeJump()(world["a2"], world["a3"], world["a1"]) assert_samples_in(board, {locations_to_bitboard(["a1"]): 1.0}) + +def test_merge_jump_imperfect_merge_scenario_1(): # Imperfect merge scenario 1 board = set_board(["a1"]) world = board.board @@ -198,6 +202,8 @@ def test_merge_jump(): }, ) + +def test_merge_jump_imperfect_merge_scenario_2(): # Imperfect merge scenario 2 # Two quantum pieces split from two sources could not be merge into to one. board = set_board(["a1", "b1"]) @@ -206,12 +212,12 @@ def test_merge_jump(): SplitJump()(world["b1"], world["b2"], world["b3"]) MergeJump()(world["a2"], world["b2"], world["c2"]) # According to matrix calculations, the ending coefficient of - # [a3, b3]: 1/4 + # [a3, b3]: 1/2 # [a3, c2]: i/2/sqrt(2) # [a3, b2]: -1/2/sqrt(2) # [b3, c2]: i/2/sqrt(2) # [b2, b3]: 1/2/sqrt(2) - # [b2, c2]: 1/4 + # [b2, c2]: 1/2 assert_sample_distribution( board, { @@ -224,6 +230,8 @@ def test_merge_jump(): }, ) + +def test_merge_jump_imperfect_merge_scenario_3(): # Imperfect merge scenario 3 # This is a simplied version of the scenario above, where we unhook a3 and b3. board = set_board(["a1", "b1"]) From 94f9d2d549d52661c80ec93be3ac72e2c12302a4 Mon Sep 17 00:00:00 2001 From: madcpf Date: Sun, 29 Oct 2023 20:28:48 -0700 Subject: [PATCH 27/31] update --- unitary/examples/quantum_chinese_chess/move_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index bae33b23..4ff04457 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -252,6 +252,10 @@ def test_jump_excluded_quantum_source_and_target(): def test_jump_basic(): + # Source is in quantum state. + board = set_board(["a1"]) + world = board.board + alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) Jump(MoveVariant.BASIC)(world["a2"], world["d1"]) board_probabilities = get_board_probability_distribution(board, 1000) assert len(board_probabilities) == 2 From 98300c57f3e4373abe0f6925fae28bb4725e707d Mon Sep 17 00:00:00 2001 From: madcpf Date: Sun, 29 Oct 2023 20:31:12 -0700 Subject: [PATCH 28/31] up --- unitary/examples/quantum_chinese_chess/move_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 4ff04457..d1ace56d 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from unitary.examples.quantum_chinese_chess.move import Move, Jump +from unitary.examples.quantum_chinese_chess.move import * from unitary.examples.quantum_chinese_chess.board import Board from unitary.examples.quantum_chinese_chess.piece import Piece import pytest From e83ac2fabbf66f7bb62e7b318bca35e178a2390d Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Thu, 9 Nov 2023 15:34:57 -0800 Subject: [PATCH 29/31] update --- unitary/examples/quantum_chinese_chess/move.py | 7 +++++++ unitary/examples/quantum_chinese_chess/move_test.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 278fd7c5..806d7566 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -114,6 +114,8 @@ class Jump(QuantumEffect): - CAPTURE - EXCLUDED - BASIC + + All types of pieces could make Jump move. """ def __init__( @@ -181,6 +183,9 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: class SplitJump(QuantumEffect): """SplitJump from source_0 to target_0 and target_1. The only accepted (default) move_variant is - BASIC. + + All types of pieces could make SplitJump move, except KING. This is implemented with a + PhasedSplit(). """ def __init__( @@ -208,6 +213,8 @@ def effect(self, *objects) -> Iterator[cirq.Operation]: class MergeJump(QuantumEffect): """MergeJump from source_0 to source_1 to target_0. The only accepted (default) move_variant is - BASIC. + + All types of pieces could make MergeJump move, except KING. """ def __init__( diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index d1ace56d..5f0abc5f 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -272,6 +272,12 @@ def test_split_jump_classical_source(): assert len(board_probabilities) == 2 assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a2"])) assert_fifty_fifty(board_probabilities, locations_to_bitboard(["a3"])) + assert world["a2"].type_ == Type.ROOK + assert world["a2"].color == Color.RED + assert world["a2"].is_entangled == True + assert world["a3"].type_ == Type.ROOK + assert world["a3"].color == Color.RED + assert world["a3"].is_entangled == True def test_split_jump_quantum_source(): @@ -288,6 +294,12 @@ def test_split_jump_quantum_source(): locations_to_bitboard(["a5"]): 0.25, }, ) + assert world["a4"].type_ == Type.ROOK + assert world["a4"].color == Color.RED + assert world["a4"].is_entangled == True + assert world["a5"].type_ == Type.ROOK + assert world["a5"].color == Color.RED + assert world["a5"].is_entangled == True def test_merge_jump_perfect_merge(): From 78e48980587d10c027c785411a5bffb691ac4335 Mon Sep 17 00:00:00 2001 From: madcpf Date: Thu, 9 Nov 2023 15:58:16 -0800 Subject: [PATCH 30/31] update --- unitary/examples/quantum_chinese_chess/move_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 5f0abc5f..1d830bbd 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -294,11 +294,7 @@ def test_split_jump_quantum_source(): locations_to_bitboard(["a5"]): 0.25, }, ) - assert world["a4"].type_ == Type.ROOK - assert world["a4"].color == Color.RED assert world["a4"].is_entangled == True - assert world["a5"].type_ == Type.ROOK - assert world["a5"].color == Color.RED assert world["a5"].is_entangled == True From 939ecc239dfce8ba764bc7813c92e7deaaba6735 Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Wed, 29 Nov 2023 14:56:42 -0800 Subject: [PATCH 31/31] update --- .../quantum_chinese_chess/move_test.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index 1d830bbd..f3ba9a66 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -143,7 +143,7 @@ def test_to_str(): def test_jump_classical(): - # Target is empty. + """Target is empty.""" board = set_board(["a1", "b1"]) world = board.board # TODO(): try move all varaibles declarations of a1 = world["a1"] into a function. @@ -156,7 +156,7 @@ def test_jump_classical(): def test_jump_capture_quantum_source(): - # Source is in quantum state. + """Source is in quantum state.""" board = set_board(["a1", "b1"]) world = board.board alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) @@ -175,7 +175,7 @@ def test_jump_capture_quantum_source(): def test_jump_capture_quantum_target(): - # Target is in quantum state. + """Target is in quantum state.""" board = set_board(["a1", "b1"]) world = board.board alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) @@ -187,7 +187,7 @@ def test_jump_capture_quantum_target(): def test_jump_capture_quantum_source_and_target(): - # Both source and target are in quantum state. + """Both source and target are in quantum state.""" board = set_board(["a1", "b1"]) world = board.board alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) @@ -216,7 +216,7 @@ def test_jump_capture_quantum_source_and_target(): def test_jump_excluded_quantum_target(): - # Target is in quantum state. + """Target is in quantum state.""" board = set_board(["a1", "b1"]) world = board.board alpha.PhasedSplit()(world["b1"], world["b2"], world["b3"]) @@ -232,7 +232,7 @@ def test_jump_excluded_quantum_target(): def test_jump_excluded_quantum_source_and_target(): - # Both source and target are in quantum state. + """Both source and target are in quantum state.""" board = set_board(["a1", "b1"]) world = board.board alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) @@ -252,7 +252,7 @@ def test_jump_excluded_quantum_source_and_target(): def test_jump_basic(): - # Source is in quantum state. + """Source is in quantum state.""" board = set_board(["a1"]) world = board.board alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) @@ -264,7 +264,7 @@ def test_jump_basic(): def test_split_jump_classical_source(): - # Source is in classical state. + """Source is in classical state.""" board = set_board(["a1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"]) @@ -281,7 +281,7 @@ def test_split_jump_classical_source(): def test_split_jump_quantum_source(): - # Source is in quantum state. + """Source is in quantum state.""" board = set_board(["a1"]) world = board.board alpha.PhasedSplit()(world["a1"], world["a2"], world["a3"]) @@ -299,7 +299,7 @@ def test_split_jump_quantum_source(): def test_merge_jump_perfect_merge(): - # Two quantum pieces split from one source could be merge back to one. + """Two quantum pieces split from one source could be merge back to one.""" board = set_board(["a1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"]) @@ -308,7 +308,7 @@ def test_merge_jump_perfect_merge(): def test_merge_jump_imperfect_merge_scenario_1(): - # Imperfect merge scenario 1 + """Imperfect merge scenario 1""" board = set_board(["a1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"]) @@ -331,8 +331,9 @@ def test_merge_jump_imperfect_merge_scenario_1(): def test_merge_jump_imperfect_merge_scenario_2(): - # Imperfect merge scenario 2 - # Two quantum pieces split from two sources could not be merge into to one. + """Imperfect merge scenario 2 + Two quantum pieces split from two sources could not be merge into to one. + """ board = set_board(["a1", "b1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"]) @@ -359,8 +360,9 @@ def test_merge_jump_imperfect_merge_scenario_2(): def test_merge_jump_imperfect_merge_scenario_3(): - # Imperfect merge scenario 3 - # This is a simplied version of the scenario above, where we unhook a3 and b3. + """Imperfect merge scenario 3. + This is a simplied version of the scenario above, where we unhook a3 and b3. + """ board = set_board(["a1", "b1"]) world = board.board SplitJump()(world["a1"], world["a2"], world["a3"])