From e03d3f3971ee254fb15bdbd43594bc72031e2624 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Thu, 4 Jul 2024 10:17:40 -0700 Subject: [PATCH 01/11] Clean up fox-in-a-hole - Add lots of comments and clean up some code paths - Hopefully should make it easier to understand. --- examples/fox_in_a_hole/fox_in_a_hole.py | 257 ++++++++++++------- examples/fox_in_a_hole/fox_in_a_hole_test.py | 29 +-- 2 files changed, 182 insertions(+), 104 deletions(-) diff --git a/examples/fox_in_a_hole/fox_in_a_hole.py b/examples/fox_in_a_hole/fox_in_a_hole.py index 61c4b134..cab4cd4e 100644 --- a/examples/fox_in_a_hole/fox_in_a_hole.py +++ b/examples/fox_in_a_hole/fox_in_a_hole.py @@ -34,38 +34,90 @@ class Game(abc.ABC): """Abstract ancestor class for Fox-in-a-hole game. - Parameters: - * hole_nr: integer - Number of holes - * seed: integer - Seed for random number generated (used for testing) + This class does the mechanical operations of the + Fox-in-a-hole game: Keeping track of the history of + moves, running the game by looping through guesses, + and printing out the history. + + This class has two variants. `ClassicalGame` + demonstrates how to play the classical version of + Fox-in-a-hole, often found in riddle and puzzle books. + `QuantumGame` is a quantum variant, where, instead of + moving from one hole to the next, the fox will move + to a superposition of both adjacent holes by using a + `Split` operator. + + This class has 4 abstract methods. These methods will + vary depending on whether the game is the classical or + quantum variant: + + - initialize_state(): This function will initialize the game. + - state_to_string(): This returns the state of the game as + a string so it can be printed out. + - check_guess(guess): This checks whether the user's guess + is correct. In the classical version, this just checks + whether the fox is in the hole. In the quantum version, + this performs a measurement to determine if the fox is in + this location. + - take_random_move(): This function should move the fox. + In the classical version, the fox moves to an adjacent hole. + In the quantum version, the fox "splits" and moves to both + adjacent holes. + + Args: + number_of_holes: The number of holes that the fox + can hide in. + seed: Seed for random number generator. (used for testing) """ - def __init__(self, hole_nr=5, seed=None): + def __init__(self, number_of_holes: int = 5, seed: int = None): + # Initialize random number generate. self.rng = np.random.default_rng(seed=seed) + # Initialize history and attributes of the object. self.history = [] + self.number_of_holes = number_of_holes - self.all_hole_names = [str(i) for i in range(hole_nr)] - - self.hole_nr = hole_nr - + # Initialize state of the game. self.state = None self.initialize_state() @abc.abstractmethod def initialize_state(self): - """Initializes the actual state.""" + """Initializes the actual state. + + This is an abstract method and will vary depending on + whether the game is played classically or quantum. + """ @abc.abstractmethod def state_to_string(self) -> str: - """Returns the string reprezentation of the state.""" + """Returns the string reprezentation of the state. + + This is an abstract method and will vary depending on + whether the game is played classically or quantum. + """ @abc.abstractmethod - def check_guess(self, guess) -> bool: - """Checks if user's guess is right and returns it as a boolean value.""" + def check_guess(self, guess: int) -> bool: + """Checks if user's guess is right and returns it as a boolean value. + + This is an abstract method and will vary depending on + whether the game is played classically or quantum. + + Args: + guess: Which number hole that the user guessed. + """ @abc.abstractmethod def take_random_move(self) -> str: - """Applies a random move on the current state. Gives back the move in string format.""" + """Applies a random move on the current state. + + This is an abstract method and will vary depending on + whether the game is played classically or quantum. + + Returns: The move in string format. + """ def history_append_state(self): """Append the current state into the history.""" @@ -82,30 +134,42 @@ def history_append_guess(self, guess): def run(self): """Handles the main game-loop of the Fox-in-a-hole game.""" max_step_nr = 10 - step_nr = 0 self.history_append_state() - while step_nr < max_step_nr: - while True: - print("Where is the fox? (0-{} or q for quit)".format(self.hole_nr - 1)) + + # Ask for user guesses until the game ends + for step_nr in range(max_step_nr): + # Get the user guess for the fox's position. + # Ask until the user inputs a valid result. + guess = -1 + while guess < 0 or guess >= self.number_of_holes: + print(f"Where is the fox? (0-{self.number_of_holes - 1} or q for quit)") input_str = input() - if input_str in ("q", "Q") or input_str in self.all_hole_names: - break - if input_str in ("q", "Q"): - print("\nQuitting.\n") - break - guess = int(input_str) + if input_str in ("q", "Q"): + print("\nQuitting.\n") + self.print_history() + try: + guess = int(input_str) + except ValueError: + print("Invalid guess.") + + # Append guess to the history self.history_append_guess(guess) + + # Check whether the guess was correct. result = self.check_guess(guess) self.history_append_state() + if result: print("\nCongratulations! You won in {} step(s).\n".format(step_nr + 1)) - break + self.print_history() + return + + # Move the fox and keep track of history. move_str = self.take_random_move() self.history_append_move(move_str) self.history_append_state() - step_nr += 1 - if step_nr == max_step_nr: - print("\nIt seems you have lost :-(. Try again.\n") + + print("\nIt seems you have lost :-(. Try again.\n") self.print_history() def print_history(self): @@ -116,11 +180,21 @@ def print_history(self): class ClassicalGame(Game): - """Classical Fox-in-a-hole game.""" + """Classical Fox-in-a-hole game. + + In this version, we play the classical version of + Fox-in-a-Hole. + + Each hole is either 0.0 (Fox not there) or 1.0 (Fox is there). + + """ def initialize_state(self): - self.state = self.hole_nr * [0.0] - index = self.rng.integers(low=0, high=self.hole_nr) + # All holes start as empty + self.state = self.number_of_holes * [0.0] + + # Pick a random hole index and put the fox in it + index = self.rng.integers(low=0, high=self.number_of_holes) self.state[index] = 1.0 def state_to_string(self) -> str: @@ -131,21 +205,32 @@ def check_guess(self, guess) -> bool: return self.state[guess] == 1.0 def take_random_move(self) -> str: - """Applies a random move on the current state. Gives back the move in string format.""" + """Applies a random move on the current state. + + Moves the fox in a random direction (either forward or backwards). + If the fox is on one end of the track (position 0 or self.number_of_holes -1), + it only has one choice. + + Returns: The move in string format.""" + + # Get where the fox started source = self.state.index(1.0) - direction = self.rng.integers(low=0, high=2) * 2 - 1 - if source == 0 and direction == -1: + + # If the fox is on the end, it has one choice. + # Otherwise, it can move either direction. + if source == 0: direction = 1 - elif source == self.hole_nr - 1 and direction == 1: + elif source == self.number_of_holes - 1: direction = -1 + else: + direction = self.rng.choice([1, -1]) + + # Move fox. self.state[source] = 0.0 self.state[source + direction] = 1.0 - if direction == -1: - dir_str = "left" - else: - dir_str = "right" - move_str = f"Moving {dir_str} from position {source}." - return move_str + + dir_str = "left" if direction == -1 else "right" + return f"Moving {dir_str} from position {source}." class Hole(enum.Enum): @@ -162,18 +247,25 @@ class QuantumGame(Game): (QuantumWorld, [QuantumObject]) -> quantum world, list of holes """ - def __init__(self, hole_nr=5, iswap=False, qprob=0.5, seed=None): + def __init__(self, number_of_holes=5, iswap=False, qprob=0.5, seed=None): + if iswap: + self.move_operation = PhasedMove() + self.split_operation = PhasedSplit() + self.swap_str = "iSWAP" + else: + self.move_operation = Move() + self.split_operation = Split() + self.swap_str = "SWAP" + self.iswap = iswap self.qprob = qprob - super().__init__(hole_nr=hole_nr, seed=seed) + super().__init__(number_of_holes=number_of_holes, seed=seed) def initialize_state(self): - index = self.rng.integers(low=0, high=self.hole_nr) + index = self.rng.integers(low=0, high=self.number_of_holes) holes = [] - for i in range(self.hole_nr): - hole = QuantumObject( - f"Hole-{i}-{i}", Hole.FOX if i == index else Hole.EMPTY - ) + for i in range(self.number_of_holes): + hole = QuantumObject(f"Hole-{i}", Hole.FOX if i == index else Hole.EMPTY) holes.append(hole) self.state = (QuantumWorld(holes, sampler=SparseSimulator()), holes) @@ -191,58 +283,44 @@ def check_guess(self, guess) -> bool: def take_random_move(self) -> str: """Applies a random move on the current state. Gives back the move in string format.""" probs = self.state[0].get_binary_probabilities(objects=self.state[1]) - non_empty_holes = [] - for i, prob in enumerate(probs): - if prob > 0: - non_empty_holes.append(i) + non_empty_holes = [i for i in range(len(probs)) if probs[i] > 0] index = self.rng.integers(low=0, high=len(non_empty_holes)) source = non_empty_holes[index] + + # Choose whether to move in one direction or both directions. if self.rng.random() < self.qprob: direction = 0 # Left & right at the same time else: - direction = self.rng.integers(low=0, high=2) * 2 - 1 # -1: left; 1:right + direction = self.rng.choice([1, -1]) # -1: left; 1:right + # If the fox is on the edge, it only has one choice if source == 0: direction = 1 - elif source == self.hole_nr - 1: + elif source == self.number_of_holes - 1: direction = -1 - if direction in (-1, 1): # Move left or right + + if direction in (-1, 1): + # Move left or right using a (Phased)Move operation target = source + direction - if self.iswap: - PhasedMove()(self.state[1][source], self.state[1][target]) - swap_str = "iSWAP" - else: - Move()(self.state[1][source], self.state[1][target]) - swap_str = "SWAP" - if direction == -1: - dir_str = "left" - else: - dir_str = "right" - move_str = f"Moving ({swap_str}-based) {dir_str} from position {source}." - else: # Move left & right (split) - if self.iswap: - PhasedSplit()( - self.state[1][source], - self.state[1][source - 1], - self.state[1][source + 1], - ) - swap_str = "iSWAP" - else: - Split()( - self.state[1][source], - self.state[1][source - 1], - self.state[1][source + 1], - ) - swap_str = "SWAP" - move_str = ( - "Splitting ({}-based) from position {} to positions {} and {}.".format( - swap_str, source, source - 1, source + 1 - ) + self.move_operation(self.state[1][source], self.state[1][target]) + dir_str = "left" if direction == -1 else "right" + return f"Moving ({self.swap_str}-based) {dir_str} from position {source}." + else: + # Move left & right (split) using a (Phased) operation + self.split_operation( + self.state[1][source], + self.state[1][source - 1], + self.state[1][source + 1], + ) + return ( + f"Splitting ({self.swap_str}-based) from position {source} " + f"to positions {source-1} and {source+1}." ) - return move_str if __name__ == "__main__": + # Create command-line arguments for Fox-in-a-hole + parser = argparse.ArgumentParser(description="Fox-in-a-hole game.") parser.add_argument( @@ -270,17 +348,19 @@ def take_random_move(self) -> str: ) args = parser.parse_args() - if args.is_quantum and args.qprob is None: - args.qprob = 0.5 + # Set defaults for arguments when not specified + if args.qprob is not None and not 0.0 < args.qprob <= 1.0: print("The probability p of a quantum move has to be: 0.0 0.0): game: Game = QuantumGame(qprob=args.qprob, iswap=args.use_iswap) print(f"Quantum Fox-in-a-hole game.") - print(f"Probability of quantum move: {args.qprob}.") + print(f"Probability of quantum move: {game.qprob}.") if args.use_iswap: print(f"Using iSWAP for moves.") else: @@ -290,4 +370,5 @@ def take_random_move(self) -> str: print("Classical Fox-in-a-hole game.") print(f"---------------------------------") + # Run Fox-in-a-hole game.run() diff --git a/examples/fox_in_a_hole/fox_in_a_hole_test.py b/examples/fox_in_a_hole/fox_in_a_hole_test.py index 8f49a7a9..d61937f1 100644 --- a/examples/fox_in_a_hole/fox_in_a_hole_test.py +++ b/examples/fox_in_a_hole/fox_in_a_hole_test.py @@ -22,7 +22,7 @@ def test_classical_game_basics(): """Simple tests for ClassicalGame.""" test_game = fh.ClassicalGame(seed=42) - assert test_game.hole_nr == 5 + assert test_game.number_of_holes == 5 assert len(test_game.history) == 0 assert test_game.state == [1.0, 0.0, 0.0, 0.0, 0.0] for i in range(5): @@ -45,7 +45,7 @@ def test_classical_game_moves(): test_game.take_random_move() assert test_game.state == [0.0, 1.0, 0.0, 0.0, 0.0] test_game.take_random_move() - assert test_game.state == [0.0, 0.0, 1.0, 0.0, 0.0] + assert test_game.state == [1.0, 0.0, 0.0, 0.0, 0.0] test_game.take_random_move() assert test_game.state == [0.0, 1.0, 0.0, 0.0, 0.0] for i in range(5): @@ -56,7 +56,7 @@ def test_classical_game_moves(): def test_quantum_game_basics(): """Simple tests for QuantumGame.""" test_game = fh.QuantumGame(seed=42) - assert test_game.hole_nr == 5 + assert test_game.number_of_holes == 5 assert len(test_game.history) == 0 probs = test_game.state[0].get_binary_probabilities(objects=test_game.state[1]) assert probs == [1.0, 0.0, 0.0, 0.0, 0.0] @@ -76,26 +76,23 @@ def test_quantum_game_basics(): def test_quantum_game_moves_q_eq_half(): - test_game = fh.QuantumGame(seed=12) + test_game = fh.QuantumGame(seed=12, qprob=100) probs = test_game.state[0].get_binary_probabilities(objects=test_game.state[1]) assert probs == [0.0, 0.0, 0.0, 1.0, 0.0] test_game.take_random_move() probs = test_game.state[0].get_binary_probabilities(objects=test_game.state[1]) - assert probs == [0.0, 0.0, 1.0, 0.0, 0.0] - test_game.take_random_move() - probs = test_game.state[0].get_binary_probabilities(objects=test_game.state[1]) assert probs[0] == 0.0 - assert probs[1] > 0.0 - assert probs[2] == 0.0 - assert probs[3] > 0.0 - assert probs[4] == 0.0 - guess = test_game.check_guess(3) + assert probs[1] == 0.0 + assert probs[2] > 0.0 + assert probs[3] == 0.0 + assert probs[4] > 0.0 + guess = test_game.check_guess(2) probs = test_game.state[0].get_binary_probabilities(objects=test_game.state[1]) assert probs[0] == 0.0 - assert probs[2] == 0.0 - assert probs[4] == 0.0 - assert (guess and probs[3] == 1.0 and probs[1] == 0.0) or ( - not guess and probs[3] == 0.0 and probs[1] == 1.0 + assert probs[1] == 0.0 + assert probs[3] == 0.0 + assert (guess and probs[2] == 1.0 and probs[4] == 0.0) or ( + not guess and probs[2] == 0.0 and probs[4] == 1.0 ) From 5b4d06e34da55d6d6386f66fc834574d5730aec6 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Fri, 12 Jul 2024 13:36:49 -0700 Subject: [PATCH 02/11] Preliminary draft of the Perimeter zone - A layout and some room descriptions for the next RPG zone. Based on the Perimeter Institute in Waterloo. - Based on the quantum AI road map, the next zone should enter tunnels under the mountains: https://quantumai.google/learn/map --- .../final_state_world_test.py | 3 +- .../quantum_perimeter.py | 222 +++++++++++++++++- 2 files changed, 222 insertions(+), 3 deletions(-) diff --git a/examples/quantum_rpg/final_state_preparation/final_state_world_test.py b/examples/quantum_rpg/final_state_preparation/final_state_world_test.py index e6b1a071..f24dc634 100644 --- a/examples/quantum_rpg/final_state_preparation/final_state_world_test.py +++ b/examples/quantum_rpg/final_state_preparation/final_state_world_test.py @@ -32,7 +32,8 @@ } # Rooms that purposely do not have a way back. -_ONE_WAY_ROOMS = {"hadamard1", "hadamard4_0", "hadamard4_1", "hadamard5", "perimeter1"} +_ONE_WAY_ROOMS = {"hadamard1", "hadamard4_0", "hadamard4_1", "hadamard5", "perimeter1", + "perimeter99"} def find_room(room_name: str): diff --git a/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py b/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py index 8b0c726f..b3e14509 100644 --- a/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py +++ b/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py @@ -22,10 +22,228 @@ label="perimeter1", title="Inside the Perimeter", description=( - "You have made it inside the Quantum Perimeter Research Facility." + "You have made it inside the Quantum Perimeter Research Facility.\n" + "What once must have been a modern and extravagent reception area\n" + "has now fallen into disrepair. Light from a large hall seeps in\n" + "from the north. Double doors lead to a theatre to the east." ), encounters=[], items=[CONSTRUCTION_SIGN], - exits={Direction.SOUTH: "hadamard17"}, + exits={Direction.EAST: "perimeter2", Direction.NORTH: "perimeter3"}, + ), + Location( + label="perimeter2", + title="Theatre of Ideas", + description=( + "A large lecture hall is filled with empty seats. The front of\n" + "the theatre is filled with a large stage and screen. Light from\n" + "a projector in the ceiling illuminates a presentation of slides\n" + "that periodically rotate with an echoing click that reveberates\n" + "through the empty hall." + ), + encounters=[], + items=[], + exits={Direction.WEST: "perimeter1", Direction.UP: "perimeter6"}, + ), + Location( + label="perimeter3", + title="Atrium", + description=( + "Diffuse sunlight seeps in through an opening far above. Several floors\n" + "of broken windows surround the rectangular atrium, extending upwards.\n" + "Vague pools of dissolved material and scattered glass shards are all\n" + "that remain within this empty space." + ), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter1", Direction.NORTH: "perimeter4"}, + ), + Location( + label="perimeter4", + title="Black Hole Bistro", + description=( + "A sign hangs crookedly over the institute's cafeteria.\n" + "Within, overturned chairs and tables fill the chaotically\n" + "arranged place. Strange radiation emanates from the\n" + "counters and serving areas." + ), + encounters=[], + items=[], + exits={ + Direction.SOUTH: "perimeter3", + Direction.NORTH: "perimeter5", + Direction.UP: "perimeter9", + }, + ), + Location( + label="perimeter5", + title="Reflection Pool", + description=("A reflection pool outside the perimeter institute."), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter4", Direction.NORTH: "perimeter20"}, + ), + Location( + label="perimeter6", + title="Theatre Seating", + description=("Second floor of the lecture hall."), + encounters=[], + items=[], + exits={Direction.DOWN: "perimeter2", Direction.NORTH: "perimeter7"}, + ), + Location( + label="perimeter7", + title="Reading Room", + description=( + "A library within the Perimeter institute. Books containing\n" + "quantapedia entries can be found here." + ), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter6", Direction.NORTH: "perimeter8"}, + ), + Location( + label="perimeter8", + title="Stairway", + description=("Stairs leading upwards."), + encounters=[], + items=[], + exits={ + Direction.UP: "perimeter11", + Direction.SOUTH: "perimeter7", + Direction.WEST: "perimeter9", + }, + ), + Location( + label="perimeter9", + title="Dining Area", + description=("Upstairs from the bistro."), + encounters=[], + items=[], + exits={ + Direction.DOWN: "perimeter4", + Direction.NORTH: "perimeter10", + Direction.EAST: "perimeter8", + }, + ), + Location( + label="perimeter10", + title="Terrace", + description=( + "From the overlook, you can see the surrounding area.\n" + "In the distance, a tunnel into the mountains of error\n" + "correction can be seen past a large forest." + ), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter9"}, + ), + Location( + label="perimeter11", + title="Stairway", + description=("Stairs lead up and down."), + encounters=[], + items=[], + exits={ + Direction.DOWN: "perimeter8", + Direction.NORTH: "perimeter12", + Direction.UP: "perimeter15", + }, + ), + Location( + label="perimeter12", + title="Hallway", + description=(""), + encounters=[], + items=[], + exits={ + Direction.SOUTH: "perimeter11", + Direction.WEST: "perimeter13", + Direction.EAST: "perimeter14", + }, + ), + Location( + label="perimeter13", + title="Theorist Office", + description=(""), + encounters=[], + items=[], + exits={Direction.EAST: "perimeter12"}, + ), + Location( + label="perimeter14", + title="Experimentalist Office", + description=(""), + encounters=[], + items=[], + exits={Direction.WEST: "perimeter12"}, + ), + Location( + label="perimeter15", + title="Rooftop Garden", + description=(""), + encounters=[], + items=[], + exits={Direction.DOWN: "perimeter11"}, + ), + Location( + label="perimeter20", + title="By the shores of a Silver Lake", + description=(""), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter5", Direction.NORTH: "perimeter21"}, + ), + Location( + label="perimeter21", + title="Bridge over Silver Lake", + description=(""), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter20", Direction.NORTH: "perimeter22"}, + ), + Location( + label="perimeter22", + title="By an old mill", + description=(""), + encounters=[], + items=[], + exits={ + Direction.SOUTH: "perimeter21", + Direction.EAST: "perimeter23", + Direction.NORTH: "perimeter24", + }, + ), + Location( + label="perimeter23", + title="Grist Mill", + description=(""), + encounters=[], + items=[], + exits={Direction.WEST: "perimeter22"}, + ), + Location( + label="perimeter24", + title="On the edge of a twisty forest", + description=("Entrance to the forest maze."), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter22", Direction.NORTH: "perimeter25"}, + ), + Location( + label="perimeter25", + title="Maze of twisty little forest passages", + description=("A maze of twisty forest passages, all alike."), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter24"}, + ), + Location( + label="perimeter99", + title="Into the Unknown", + description=("Entrance to the next zone."), + encounters=[], + items=[], + exits={Direction.SOUTH: "perimeter25"}, ), ] From 7ee60051ee6465081a892a3da739959bd1a0aaa5 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Sat, 13 Jul 2024 05:19:26 -0700 Subject: [PATCH 03/11] Update quantum_perimeter.py --- .../quantum_rpg/final_state_preparation/quantum_perimeter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py b/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py index b3e14509..f62e1605 100644 --- a/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py +++ b/examples/quantum_rpg/final_state_preparation/quantum_perimeter.py @@ -38,7 +38,7 @@ "A large lecture hall is filled with empty seats. The front of\n" "the theatre is filled with a large stage and screen. Light from\n" "a projector in the ceiling illuminates a presentation of slides\n" - "that periodically rotate with an echoing click that reveberates\n" + "that periodically rotate with an echoing click that reverberates\n" "through the empty hall." ), encounters=[], From acbeff6e7a9a08aae092c005a84441a2675a347a Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Mon, 22 Jul 2024 14:43:40 -0700 Subject: [PATCH 04/11] move away from DensityMatrixSimulator --- unitary/alpha/quantum_world.py | 33 +++++++++++++++++------- unitary/alpha/quantum_world_test.py | 40 ++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 04c87975..b8ba55ce 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -637,12 +637,13 @@ def get_binary_probabilities( return binary_probs def density_matrix( - self, objects: Optional[Sequence[QuantumObject]] = None + self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 1000 ) -> np.ndarray: """Simulates the density matrix of the given objects. Parameters: objects: List of QuantumObjects (currently only qubits are supported) + count: Number of measurements Returns: The density matrix of the specified objects. @@ -654,23 +655,27 @@ def density_matrix( [obj.qubit.name for obj in objects] if objects is not None else [] ) unspecified_names = set(self.object_name_dict.keys()) - set(specified_names) + # Make sure we have all objects, starting with the specified ones in the given order. ordered_names = specified_names + list(unspecified_names) - ordered_qubits = [self.object_name_dict[name].qubit for name in ordered_names] + ordered_objects = [self.object_name_dict[name] for name in ordered_names] - simulator = cirq.DensityMatrixSimulator() - qubit_order = cirq.QubitOrder.explicit( - ordered_qubits, fallback=cirq.QubitOrder.DEFAULT - ) - result = simulator.simulate(self.circuit, qubit_order=qubit_order) + # Peek the current world `count` times and get the results. + histogram = self.get_correlated_histogram(ordered_objects, count) + + # Get an estimate of the state vector. + state_vector = np.array([0.] * (2**num_all_qubits)) + for key, val in histogram.items(): + state_vector += self.__to_state_vector__(key) * np.sqrt(val * 1.0 / count) + density_matrix = np.outer(state_vector, state_vector) if num_shown_qubits == num_all_qubits: - return result.final_density_matrix + return density_matrix else: # We trace out the unspecified qubits. # The reshape is required by the partial_trace method. traced_density_matrix = cirq.partial_trace( - result.final_density_matrix.reshape((2, 2) * num_all_qubits), + density_matrix.reshape((2, 2) * num_all_qubits), range(num_shown_qubits), ) # Reshape back to a 2-d matrix. @@ -703,3 +708,13 @@ def __getitem__(self, name: str) -> QuantumObject: if not quantum_object: raise KeyError(f"{name} did not exist in this world.") return quantum_object + + def __to_state_vector__(self, input: tuple) -> np.ndarray: + """ Converts the given tuple (of length N) to the corresponding state vector (of length 2**N). + e.g. (0, 1) -> [0, 1, 0, 0] + """ + num = len(input) + index = int(''.join([str(i) for i in input]),2) + state_vector = np.array([0.] * (2**num)) + state_vector[index] = 1.0 + return state_vector diff --git a/unitary/alpha/quantum_world_test.py b/unitary/alpha/quantum_world_test.py index d168c077..f8880c9d 100644 --- a/unitary/alpha/quantum_world_test.py +++ b/unitary/alpha/quantum_world_test.py @@ -895,13 +895,26 @@ def test_save_and_restore_snapshot(simulator, compile_to_qubits): world.restore_last_snapshot() -def test_density_matrix(): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ], +) +def test_density_matrix(simulator, compile_to_qubits): rho_green = np.reshape([0, 0, 0, 1], (2, 2)) rho_red = np.reshape([1, 0, 0, 0], (2, 2)) light1 = alpha.QuantumObject("green", Light.GREEN) light2 = alpha.QuantumObject("red1", Light.RED) light3 = alpha.QuantumObject("red2", Light.RED) - board = alpha.QuantumWorld([light1, light2, light3]) + board = alpha.QuantumWorld( + [light1, light2, light3], + sampler=simulator(), + compile_to_qubits=compile_to_qubits, + ) testing.assert_array_equal(board.density_matrix(objects=[light1]), rho_green) testing.assert_array_equal(board.density_matrix(objects=[light2]), rho_red) @@ -923,13 +936,26 @@ def test_density_matrix(): ) -def test_measure_entanglement(): +@pytest.mark.parametrize( + ("simulator", "compile_to_qubits"), + [ + (cirq.Simulator, False), + (cirq.Simulator, True), + # Cannot use SparseSimulator without `compile_to_qubits` due to issue #78. + (alpha.SparseSimulator, True), + ], +) +def test_measure_entanglement(simulator, compile_to_qubits): rho_green = np.reshape([0, 0, 0, 1], (2, 2)) rho_red = np.reshape([1, 0, 0, 0], (2, 2)) light1 = alpha.QuantumObject("red1", Light.RED) light2 = alpha.QuantumObject("green", Light.GREEN) light3 = alpha.QuantumObject("red2", Light.RED) - board = alpha.QuantumWorld([light1, light2, light3]) + board = alpha.QuantumWorld( + [light1, light2, light3], + sampler=simulator(), + compile_to_qubits=compile_to_qubits, + ) # S_1 + S_2 - S_12 = 0 + 0 - 0 = 0 for all three cases. assert round(board.measure_entanglement(light1, light2)) == 0.0 @@ -942,8 +968,8 @@ def test_measure_entanglement(): assert not all(result[0] == 0 for result in results) assert (result[0] == result[1] for result in results) # S_1 + S_2 - S_12 = 0 + 1 - 1 = 0 - assert round(board.measure_entanglement(light1, light2), 3) == 0.0 + assert round(board.measure_entanglement(light1, light2), 1) == 0.0 # S_1 + S_2 - S_12 = 0 + 1 - 1 = 0 - assert round(board.measure_entanglement(light1, light3), 3) == 0.0 + assert round(board.measure_entanglement(light1, light3), 1) == 0.0 # S_1 + S_2 - S_12 = 1 + 1 - 0 = 2 - assert round(board.measure_entanglement(light2, light3), 3) == 2.0 + assert round(board.measure_entanglement(light2, light3), 1) == 2.0 From 7c0cea932c55ce6534ac1cd8d1c773bf4010fc25 Mon Sep 17 00:00:00 2001 From: Pengfei Chen Date: Mon, 22 Jul 2024 14:49:27 -0700 Subject: [PATCH 05/11] update format --- unitary/alpha/quantum_world.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index b8ba55ce..7d8d5ad0 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -664,7 +664,7 @@ def density_matrix( histogram = self.get_correlated_histogram(ordered_objects, count) # Get an estimate of the state vector. - state_vector = np.array([0.] * (2**num_all_qubits)) + state_vector = np.array([0.0] * (2**num_all_qubits)) for key, val in histogram.items(): state_vector += self.__to_state_vector__(key) * np.sqrt(val * 1.0 / count) density_matrix = np.outer(state_vector, state_vector) @@ -710,11 +710,11 @@ def __getitem__(self, name: str) -> QuantumObject: return quantum_object def __to_state_vector__(self, input: tuple) -> np.ndarray: - """ Converts the given tuple (of length N) to the corresponding state vector (of length 2**N). + """Converts the given tuple (of length N) to the corresponding state vector (of length 2**N). e.g. (0, 1) -> [0, 1, 0, 0] """ num = len(input) - index = int(''.join([str(i) for i in input]),2) - state_vector = np.array([0.] * (2**num)) + index = int("".join([str(i) for i in input]), 2) + state_vector = np.array([0.0] * (2**num)) state_vector[index] = 1.0 return state_vector From 0dea2403e766ef01c6928882798d084129cc2697 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Wed, 24 Jul 2024 06:57:13 -0700 Subject: [PATCH 06/11] Update fox_in_a_hole.py --- examples/fox_in_a_hole/fox_in_a_hole.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/fox_in_a_hole/fox_in_a_hole.py b/examples/fox_in_a_hole/fox_in_a_hole.py index cab4cd4e..ff92687f 100644 --- a/examples/fox_in_a_hole/fox_in_a_hole.py +++ b/examples/fox_in_a_hole/fox_in_a_hole.py @@ -19,6 +19,7 @@ import enum import numpy as np import argparse +from typing import Optional from unitary.alpha import ( QuantumObject, @@ -70,7 +71,7 @@ class Game(abc.ABC): seed: Seed for random number generator. (used for testing) """ - def __init__(self, number_of_holes: int = 5, seed: int = None): + def __init__(self, number_of_holes: int = 5, seed: Optional[int] = None): # Initialize random number generate. self.rng = np.random.default_rng(seed=seed) @@ -191,7 +192,7 @@ class ClassicalGame(Game): def initialize_state(self): # All holes start as empty - self.state = self.number_of_holes * [0.0] + self.state = [0.0] * self.number_of_holes # Pick a random hole index and put the fox in it index = self.rng.integers(low=0, high=self.number_of_holes) @@ -247,7 +248,7 @@ class QuantumGame(Game): (QuantumWorld, [QuantumObject]) -> quantum world, list of holes """ - def __init__(self, number_of_holes=5, iswap=False, qprob=0.5, seed=None): + def __init__(self, number_of_holes: int = 5, iswap: bool = False, qprob:float = 0.5, seed: Optional[int] = None): if iswap: self.move_operation = PhasedMove() self.split_operation = PhasedSplit() @@ -283,7 +284,7 @@ def check_guess(self, guess) -> bool: def take_random_move(self) -> str: """Applies a random move on the current state. Gives back the move in string format.""" probs = self.state[0].get_binary_probabilities(objects=self.state[1]) - non_empty_holes = [i for i in range(len(probs)) if probs[i] > 0] + non_empty_holes = [i for i, p in enumerate(probs) if p > 0] index = self.rng.integers(low=0, high=len(non_empty_holes)) source = non_empty_holes[index] From fec231bdf821256567bbd63144b61d75d947bd26 Mon Sep 17 00:00:00 2001 From: Peter Solodov Date: Thu, 25 Jul 2024 12:36:50 -0400 Subject: [PATCH 07/11] updated python to 3.12 --- .github/workflows/pylint.yml | 2 +- .github/workflows/pythonpackage.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 75de73da..d8a0da8b 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: - python-version: '3.10' + python-version: '3.12' architecture: 'x64' - name: Install Pylint run: | diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index ccc92ae1..c7820cae 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -62,6 +62,6 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.12' - name: Doc check run: dev_tools/nbfmt From 03b34bc68c44bb9cfb57bbbe1a658649d3c61462 Mon Sep 17 00:00:00 2001 From: Peter Solodov Date: Thu, 25 Jul 2024 13:11:36 -0400 Subject: [PATCH 08/11] added setuptools dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 99b49c5d..0251de69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ cirq-core>=0.15.0 cirq-google>=0.15.0 # When changing Cirq requirements be sure to update dev_tools/write-ci-requirements.py +setuptools scipy ipython black From c36704b2d1b7b03c2f69b75b040b4f46f297537b Mon Sep 17 00:00:00 2001 From: Peter Solodov Date: Thu, 25 Jul 2024 13:21:21 -0400 Subject: [PATCH 09/11] added note about python 3.12 and venv --- README.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ae26d420..b7531472 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,23 @@ found in the `quantum_chess` directory. ## Installation and Documentation +Unitary uses Python 3.12. It's recommended to use a virtual environment for installing Unitary to +avoid interfering with other system packages: + +```sh +python3.12 -v venv ~/unitary +source ~/unitary/bin/activate +``` + +Unitary can then be installed within that virtual environment. + Unitary is not available as a PyPI package. Please clone this repository and install from source: - cd unitary/ - pip install . +```sh +cd unitary/ +pip install . +``` Documentation is available at https://quantumai.google/cirq/experiments. @@ -30,9 +42,11 @@ upper right corner). You can then clone the repository into your development environment by using (substitute USER with your github username) - git clone https://github.com/USER/unitary.git - cd unitary - git remote add upstream https://github.com/quantumlib/unitary.git +```sh +git clone https://github.com/USER/unitary.git +cd unitary +git remote add upstream https://github.com/quantumlib/unitary.git +``` This will clone your fork so that you can work on it, while marking the name 'upstream' as the original repository. @@ -40,17 +54,23 @@ name 'upstream' as the original repository. You can then pull from the original and update your fork, for instance, by doing this: - git pull upstream main - git push origin main +```sh +git pull upstream main +git push origin main +``` In order to push changes to unitary, create a branch in your fork: - git checkout -b BRANCH_NAME +```sh +git checkout -b BRANCH_NAME +``` Perform your changes, then commit (i.e. `git commit -a`) then push to your fork: - git push origin BRANCH_NAME +```sh +git push origin BRANCH_NAME +``` This will give you a link to create a PR (pull request). Create this pull request and pick some reviewers. Once approved, it will be merged into the original repository. From ad2b76e9438f9ac80bf9825e4b14e40eb9ecf183 Mon Sep 17 00:00:00 2001 From: Peter Solodov Date: Tue, 30 Jul 2024 12:37:22 -0400 Subject: [PATCH 10/11] bump required python --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a1ceba7..55dd99a3 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def _parse_requirements(path: pathlib.Path): url="http://github.com/quantumlib/unitary", author="Quantum AI team and collaborators", author_email="quantum-chess-engineering@googlegroups.com", - python_requires=">=3.6.0", + python_requires=">=3.12.0", install_requires=install_requires, license="Apache 2", description="", From 5afde999aa0290dd6f3406691e91317d94b6c239 Mon Sep 17 00:00:00 2001 From: madcpf Date: Tue, 30 Jul 2024 15:16:05 -0700 Subject: [PATCH 11/11] update comment --- unitary/alpha/quantum_world.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/unitary/alpha/quantum_world.py b/unitary/alpha/quantum_world.py index 7d8d5ad0..6235b908 100644 --- a/unitary/alpha/quantum_world.py +++ b/unitary/alpha/quantum_world.py @@ -639,11 +639,17 @@ def get_binary_probabilities( def density_matrix( self, objects: Optional[Sequence[QuantumObject]] = None, count: int = 1000 ) -> np.ndarray: - """Simulates the density matrix of the given objects. + """Simulates the density matrix of the given objects. We assume that the overall state of + the quantum world (including all quantum objects in it) could be described by one pure + state. To calculate the density matrix of the given quantum objects, we would always measure/ + peek the quantum world for `count` times, deduce the (pure) state vector based on the results, + then the density matrix is its outer product. We will then trace out the un-needed quantum + objects before returning the density matrix. Parameters: - objects: List of QuantumObjects (currently only qubits are supported) - count: Number of measurements + objects: List of QuantumObjects (currently only qubits are supported). If not specified, + all quantum objects' density matrix will be returned. + count: Number of measurements. Returns: The density matrix of the specified objects.