Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Quantum Chinese Chess] Add SplitJump and MergeJump #166

Merged
merged 33 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions unitary/examples/quantum_chinese_chess/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class Jump(QuantumEffect):
- CAPTURE
- EXCLUDED
- BASIC

All types of pieces could make Jump move.
"""

def __init__(
Expand Down Expand Up @@ -176,3 +178,62 @@ def effect(self, *objects) -> Iterator[cirq.Operation]:
target_0.reset(source_0)
source_0.reset()
return iter(())


class SplitJump(QuantumEffect):
"""SplitJump from source_0 to target_0 and target_1. The only accepted (default) move_variant is
- BASIC.
madcpf marked this conversation as resolved.
Show resolved Hide resolved

All types of pieces could make SplitJump move, except KING. This is implemented with a
PhasedSplit().
"""

def __init__(
self,
):
return

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
# Make the split jump.
source_0.is_entangled = True
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)
return iter(())


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__(
self,
):
return

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
# Make the merge jump.
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This semantic is kind of confusing. I am not totally sure if this is correct. Remind me what reset is supposed to do again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It copies the "type", "color" and "is_entangled" properties from source_0 to target_0.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. And we are assuming that type, color are the same for source_0 and source_1.

Couldn't is_entangled be different for source_0 and source_1 though?
Also, do we ever fix the type/color for the source squares? Like, they may be empty now, right?

I don't think this logic is correct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Doug, yes, earlier in chess.py we've checked to make sure source_0 and source_1 have the same type, color, and is_entangled = true (maybe later we could support classical to merge with entangled piece).

The type/color/is_entangled for source squares are fixed through the method "update_board_by_sampling()" in chess.py, which will be called after each move. There are cases that they won't be reset to empty (see test cases for those scenarios).

The source piece for SplitJump will also be fixed through update_board_by_sampling().

return iter(())
136 changes: 128 additions & 8 deletions unitary/examples/quantum_chinese_chess/move_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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"])
Expand All @@ -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"])
Expand All @@ -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"])
Expand Down Expand Up @@ -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"])
Expand All @@ -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"])
Expand All @@ -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"])
Expand All @@ -261,3 +261,123 @@ def test_jump_basic():
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_classical_source():
madcpf marked this conversation as resolved.
Show resolved Hide resolved
"""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"]))
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():
"""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,
},
)
assert world["a4"].is_entangled == True
assert world["a5"].is_entangled == True


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
SplitJump()(world["a1"], world["a2"], world["a3"])
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
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.0 / 4,
locations_to_bitboard(["a6"]): 3.0 / 8,
locations_to_bitboard(["a4"]): 3.0 / 8,
},
)


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"])
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/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/2
assert_sample_distribution(
board,
{
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,
},
)


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"])
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.0 / 4,
locations_to_bitboard(["c2"]): 1.0 / 4,
locations_to_bitboard(["b2"]): 1.0 / 4,
locations_to_bitboard(["b2", "c2"]): 1.0 / 4,
},
)
Loading