Skip to content

Commit

Permalink
initial set
Browse files Browse the repository at this point in the history
  • Loading branch information
Pengfei Chen committed Oct 17, 2023
1 parent 04e6936 commit 0e6a382
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 31 deletions.
11 changes: 11 additions & 0 deletions unitary/alpha/quantum_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,17 @@ 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.
"""
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:
Expand Down
2 changes: 1 addition & 1 deletion unitary/examples/quantum_chinese_chess/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,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)
Expand Down
91 changes: 72 additions & 19 deletions unitary/examples/quantum_chinese_chess/chess.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
MoveType,
MoveVariant,
)
from unitary.examples.quantum_chinese_chess.move import Move
from unitary.examples.quantum_chinese_chess.move import Jump
import readline

# List of accepable commands.
_HELP_TEXT = """
Expand Down Expand Up @@ -235,7 +236,7 @@ def classify_move(
quantum_path_pieces_1: the list of names of quantum pieces from source_0 to target_1 (for split) or
from source_1 to target_0 (for merge) (excluded)
"""
move_type = MoveType.UNSPECIFIED_STANDARD
move_type = MoveType.UNSPECIFIED
move_variant = MoveVariant.UNSPECIFIED

source = self.board.board[sources[0]]
Expand All @@ -257,16 +258,20 @@ def classify_move(
# This handles all classical cases, where no quantum piece is envolved.
# We don't need to further classify MoveVariant types since all classical cases
# will be handled in a similar way.
return MoveType.CLASSICAL, MoveVariant.UNSPECIFIED
return MoveType.CLASSICAL, MoveVariant.CLASSICAL
else:
# If any of the source or target is entangled, this move is a JUMP.
move_type = MoveType.JUMP
else:
# If there is any quantum path pieces, this move is a SLIDE.
move_type = MoveType.SLIDE

if source.type_ == Type.CANNON and (
len(classical_path_pieces_0) == 1 or len(quantum_path_pieces_0) > 0
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 move,
# so the current case has quantum piece(s) envolved.
Expand Down Expand Up @@ -381,6 +386,9 @@ def apply_move(self, str_to_parse: str) -> None:
quantum_pieces_1,
)

print(move_type, " ", move_variant)

# Apply the move accoding to its type.
if move_type == MoveType.CLASSICAL:
if source_0.type_ == Type.KING:
# Update the locations of KING.
Expand All @@ -390,34 +398,74 @@ 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()
# TODO(): only make such prints for a certain debug level.
print("Classical move.")
Jump(move_variant)(source_0, target_0)
elif move_type == MoveType.JUMP:
Jump(move_variant)(source_0, target_0)
# TODO(): apply other move types.

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":
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
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) -> List[float]:
"""After quantum moves, there might be pieces that:
- is actually empty, but their classical properties is not cleared; or
- is actually classically occupied, but their is_entangled state is not updated.
This method is called after each quantum move, and runs (100x) sampling of the board
to identify and fix those cases.
"""
# TODO(): return the sampled probabilities and pass it into the print method
# of the board to print it together with the board.
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(): This threshold does not actually work right now since we have 100 sampling.
# Change it to be more meaningful values maybe when we do error mitigation.
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."""
Expand All @@ -433,15 +481,20 @@ 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()
print(self.board)
move_success, output = self.next_move()
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)
# TODO(): maybe we should not check game_over() when an undo is made.
# Check if the game is over.
self.game_over()
# TODO(): no need to do sampling if the last move was CLASSICAL.
self.update_board_by_sampling()
print(self.board)
if self.game_state == GameState.CONTINUES:
# If the game continues, switch the player.
self.current_player = 1 - self.current_player
Expand Down
4 changes: 2 additions & 2 deletions unitary/examples/quantum_chinese_chess/chess_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,11 @@ def test_classify_move_success(monkeypatch):
# classical
assert game.classify_move(["h9"], ["g7"], [], [], [], []) == (
MoveType.CLASSICAL,
MoveVariant.UNSPECIFIED,
MoveVariant.CLASSICAL,
)
assert game.classify_move(["b2"], ["b9"], ["b7"], [], [], []) == (
MoveType.CLASSICAL,
MoveVariant.UNSPECIFIED,
MoveVariant.CLASSICAL,
)

# jump basic
Expand Down
15 changes: 8 additions & 7 deletions unitary/examples/quantum_chinese_chess/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -110,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]
Expand Down
77 changes: 76 additions & 1 deletion unitary/examples/quantum_chinese_chess/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@
# 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
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


# 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."""

Expand Down Expand Up @@ -101,3 +106,73 @@ def to_str(self, verbose_level: int = 1) -> 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
"""

def __init__(
self,
move_variant: MoveVariant,
):
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 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]
# 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 clear set it to be EMPTY, and the jump
# could not be made.
source_0.reset()
print("Jump move: 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]
# 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 a classically OCCUPIED, and
# the jump could not be made.
print("Jump move: 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()

# 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(())
5 changes: 4 additions & 1 deletion unitary/examples/quantum_chinese_chess/piece.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ def __str__(self):

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 provided, then its type_, color, and is_entangled is copied,
otherwise set the current piece to be a classically 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

0 comments on commit 0e6a382

Please sign in to comment.