def test_path_qubits(): """Source and target should be in the same line, otherwise ValueError should be returned.""" b = qb.CirqBoard( u.squares_to_bitboard(['a1', 'b3', 'c4', 'd5', 'e6', 'f7'])) assert b.path_qubits("b3", "f7") == [bit_to_qubit(square_to_bit('c4')), \ bit_to_qubit(square_to_bit('d5')), \ bit_to_qubit(square_to_bit('e6'))] with pytest.raises(ValueError): b.path_qubits("a1", "b3") with pytest.raises(ValueError): b.path_qubits("c4", "a1")
def apply(self, move: Move): """Applies a move to the board.""" s = self._pieces[move.source] # Defaults to a BASIC JUMP move if not specified if not move.move_type: move.move_type = enums.MoveType.JUMP if not move.move_variant: move.move_variant = enums.MoveVariant.BASIC # Call the quantum board to apply the move meas = self.board.do_move(move) # Cache the probability distribution self._probs = self.board.get_probability_distribution(self.reps) # Set the turn to be the next player self.white_moves = not self.white_moves # If the move was not successful, return # Otherwise, update classical bits if not meas: return meas # Assume that squares with low probability are empty if self._probs[bu.square_to_bit(move.source)] < 0.01: self._pieces[move.source] = c.EMPTY # Update classical bits self._pieces[move.target] = s if move.target2: self._pieces[move.target2] = s if move.source2 and self._probs[bu.square_to_bit(move.source2)] < 0.01: self._pieces[move.source2] = c.EMPTY # Update en passant and castling flags self.ep_flag = self._ep_flag(move, s) if s == c.KING: self.castling_flags["O-O"] = False self.castling_flags["O-O-O"] = False if s == -c.KING: self.castling_flags["o-o"] = False self.castling_flags["o-o-o"] = False if move.source == "a1" or move.target == "a1": self.castling_flags["O-O-O"] = False if move.source == "a8" or move.target == "a8": self.castling_flags["o-o-o"] = False if move.source == "h1" or move.target == "h1": self.castling_flags["O-O"] = False if move.source == "h8" or move.target == "h8": self.castling_flags["o-o"] = False return meas
def test_path_qubits(): """Source and target should be in the same line, otherwise ValueError should be returned.""" b = qb.CirqBoard( u.squares_to_bitboard(["a1", "b3", "c4", "d5", "e6", "f7"])) assert b.path_qubits("b3", "f7") == [ bit_to_qubit(square_to_bit("c4")), bit_to_qubit(square_to_bit("d5")), bit_to_qubit(square_to_bit("e6")), ] with pytest.raises(ValueError): b.path_qubits("a1", "b3") with pytest.raises(ValueError): b.path_qubits("c4", "a1")
def test_get_probability_distribution_split_jump_first_move_pre_cached(board): b = board(u.squares_to_bitboard(["a1", "b1"])) # Cache a split jump in advance. cache_key = CacheKey(enums.MoveType.SPLIT_JUMP, 100) b.cache_results(cache_key) m1 = move.Move( "b1", "c1", target2="d1", move_type=enums.MoveType.SPLIT_JUMP, move_variant=enums.MoveVariant.BASIC, ) b.do_move(m1) b.clear_debug_log() # Expected probability with the cache applied expected_probs = [0] * 64 expected_probs[square_to_bit("a1")] = 1 expected_probs[square_to_bit("b1")] = 0 expected_probs[square_to_bit("c1")] = b.cache[cache_key]["target"] expected_probs[square_to_bit("d1")] = b.cache[cache_key]["target2"] # Get probability distribution should apply the cache without rerunning _generate_accumulations. probs = b.get_probability_distribution(100, use_cache=True) full_squares = b.get_full_squares_bitboard(100, use_cache=True) empty_squares = b.get_empty_squares_bitboard(100, use_cache=True) assert probs == expected_probs # Check that the second run and getting full and empty bitboards did not trigger any new logs. assert len(b.debug_log) == 0 # Check bitboard updated correctly assert not nth_bit_of(square_to_bit("b1"), full_squares) assert not nth_bit_of(square_to_bit("c1"), full_squares) assert not nth_bit_of(square_to_bit("d1"), full_squares) assert nth_bit_of(square_to_bit("b1"), empty_squares)
def test_superposition_slide_move2(board): """Tests a basic slide through a superposition of two pieces. Splits b3 and c3 to b2/b1 and c2/c1 then slides a1 to d1. """ b = board.with_state(u.squares_to_bitboard(['a1', 'b3', 'c3'])) assert b.perform_moves( 'b3b2b1:SPLIT_JUMP:BASIC', 'c3c2c1:SPLIT_JUMP:BASIC', 'a1e1:SLIDE:BASIC', ) possibilities = [ u.squares_to_bitboard(['a1', 'b1', 'c1']), u.squares_to_bitboard(['a1', 'b1', 'c2']), u.squares_to_bitboard(['a1', 'b2', 'c1']), u.squares_to_bitboard(['e1', 'b2', 'c2']) ] samples = b.sample(100) assert (all(sample in possibilities for sample in samples)) probs = b.get_probability_distribution(10000) assert_fifty_fifty(probs, u.square_to_bit('b2')) assert_fifty_fifty(probs, u.square_to_bit('b1')) assert_fifty_fifty(probs, u.square_to_bit('c2')) assert_fifty_fifty(probs, u.square_to_bit('c1')) assert_prob_about(probs, u.square_to_bit('a1'), 0.75) assert_prob_about(probs, u.square_to_bit('e1'), 0.25)
def test_superposition_slide_move2(): """Tests a basic slide through a superposition of two pieces. Splits b3 and c3 to b2/b1 and c2/c1 then slides a1 to d1. """ b = simulator(u.squares_to_bitboard(["a1", "b3", "c3"])) assert b.perform_moves( "b3^b2b1:SPLIT_JUMP:BASIC", "c3^c2c1:SPLIT_JUMP:BASIC", "a1e1:SLIDE:BASIC", ) possibilities = [ u.squares_to_bitboard(["a1", "b1", "c1"]), u.squares_to_bitboard(["a1", "b1", "c2"]), u.squares_to_bitboard(["a1", "b2", "c1"]), u.squares_to_bitboard(["e1", "b2", "c2"]), ] samples = b.sample(100) assert all(sample in possibilities for sample in samples) probs = b.get_probability_distribution(10000) assert_fifty_fifty(probs, u.square_to_bit("b2")) assert_fifty_fifty(probs, u.square_to_bit("b1")) assert_fifty_fifty(probs, u.square_to_bit("c2")) assert_fifty_fifty(probs, u.square_to_bit("c1")) assert_prob_about(probs, u.square_to_bit("a1"), 0.75) assert_prob_about(probs, u.square_to_bit("e1"), 0.25) board_probs = b.get_board_probability_distribution(10000) assert len(board_probs) == len(possibilities) for possibility in possibilities: assert_prob_about(board_probs, possibility, 0.25)
def test_split_move(move_type, board): b = board(u.squares_to_bitboard(["a1"])) b.do_move( move.Move( "a1", "a3", target2="c1", move_type=move_type, move_variant=enums.MoveVariant.BASIC, )) samples = b.sample(100) assert_this_or_that(samples, u.squares_to_bitboard(["a3"]), u.squares_to_bitboard(["c1"])) probs = b.get_probability_distribution(5000) assert_fifty_fifty(probs, qb.square_to_bit("a3")) assert_fifty_fifty(probs, qb.square_to_bit("c1")) board_probs = b.get_board_probability_distribution(5000) assert len(board_probs) == 2 assert_fifty_fifty(board_probs, u.squares_to_bitboard(["a3"])) assert_fifty_fifty(board_probs, u.squares_to_bitboard(["c1"])) # Test doing a jump after a split move m = move.Move("c1", "d1", move_type=enums.MoveType.JUMP, move_variant=enums.MoveVariant.BASIC) assert b.do_move(m) samples = b.sample(100) assert_this_or_that(samples, u.squares_to_bitboard(["a3"]), u.squares_to_bitboard(["d1"])) probs = b.get_probability_distribution(5000) assert_fifty_fifty(probs, u.square_to_bit("a3")) assert_fifty_fifty(probs, u.square_to_bit("d1")) board_probs = b.get_board_probability_distribution(5000) assert len(board_probs) == 2 assert_fifty_fifty(board_probs, u.squares_to_bitboard(["a3"])) assert_fifty_fifty(board_probs, u.squares_to_bitboard(["d1"]))
def test_get_probability_distribution_split_jump_pre_cached(board): b = board(u.squares_to_bitboard(['a1', 'b1'])) # Cache a split jump in advance. cache_key = CacheKey(enums.MoveType.SPLIT_JUMP, 100) b.cache_results(cache_key) m1 = move.Move('a1', 'a2', move_type=enums.MoveType.JUMP, move_variant=enums.MoveVariant.BASIC) m2 = move.Move('b1', 'c1', target2='d1', move_type=enums.MoveType.SPLIT_JUMP, move_variant=enums.MoveVariant.BASIC) b.do_move(m1) probs = b.get_probability_distribution(100) b.do_move(m2) b.clear_debug_log() # Expected probability with the cache applied probs[square_to_bit('b1')] = 0 probs[square_to_bit('c1')] = b.cache[cache_key]["target"] probs[square_to_bit('d1')] = b.cache[cache_key]["target2"] # Get probability distribution should apply the cache without rerunning _generate_accumulations. probs2 = b.get_probability_distribution(100, use_cache=True) full_squares = b.get_full_squares_bitboard(100, use_cache=True) empty_squares = b.get_empty_squares_bitboard(100, use_cache=True) assert probs == probs2 # Check that the second run and getting full and empty bitboards did not trigger any new logs. assert len(b.debug_log) == 0 # Check bitboard updated correctly assert not nth_bit_of(square_to_bit('b1'), full_squares) assert not nth_bit_of(square_to_bit('c1'), full_squares) assert not nth_bit_of(square_to_bit('d1'), full_squares) assert nth_bit_of(square_to_bit('b1'), empty_squares)
def test_square_to_bit(): assert u.square_to_bit('a1') == 0 assert u.square_to_bit('a2') == 8 assert u.square_to_bit('b2') == 9 assert u.square_to_bit('b1') == 1 assert u.square_to_bit('h8') == 63
def test_square_to_bit(): assert u.square_to_bit("a1") == 0 assert u.square_to_bit("a2") == 8 assert u.square_to_bit("b2") == 9 assert u.square_to_bit("b1") == 1 assert u.square_to_bit("h8") == 63
def do_move(self, m: move.Move) -> int: """Performs a move on the quantum board. Based on the type and variant of the move requested, this function augments the circuit, classical registers, and post-selection criteria to perform the board. Returns: The measurement that was performed, or 1 if no measurement was required. """ if not m.move_type: raise ValueError('No Move defined') if m.move_type == enums.MoveType.NULL_TYPE: raise ValueError('Move has null type') if m.move_type == enums.MoveType.UNSPECIFIED_STANDARD: raise ValueError('Move type is unspecified') # Reset accumulations here because function has conditional return branches self.accumulations_repetitions = None # Add move to the move move_history self.move_history.append(m) sbit = square_to_bit(m.source) tbit = square_to_bit(m.target) squbit = bit_to_qubit(sbit) tqubit = bit_to_qubit(tbit) if (m.move_variant == enums.MoveVariant.CAPTURE or m.move_type == enums.MoveType.PAWN_EP or m.move_type == enums.MoveType.PAWN_CAPTURE): # TODO: figure out if it is a deterministic capture. for val in list(self.allowed_pieces): self.allowed_pieces.add(val - 1) if m.move_type == enums.MoveType.PAWN_EP: # For en passant, first determine the square of the pawn being # captured, which should be next to the target. if m.target[1] == '6': epbit = square_to_bit(m.target[0] + '5') elif m.target[1] == '2': epbit = square_to_bit(m.target[0] + '4') else: raise ValueError(f'Invalid en passant target {m.target}') epqubit = bit_to_qubit(epbit) # For the classical version, set the bits appropriately if (epqubit not in self.entangled_squares and squbit not in self.entangled_squares and tqubit not in self.entangled_squares): if (not nth_bit_of(epbit, self.state) or not nth_bit_of(sbit, self.state) or nth_bit_of(tbit, self.state)): raise ValueError('Invalid classical e.p. move') self.state = set_nth_bit(epbit, self.state, False) self.state = set_nth_bit(sbit, self.state, False) self.state = set_nth_bit(tbit, self.state, True) return 1 # If any squares are quantum, it's a quantum move self.add_entangled(squbit, tqubit, epqubit) # Capture e.p. post-select on the source if m.move_variant == enums.MoveVariant.CAPTURE: is_there = self.post_select_on(squbit) if not is_there: return 0 self.add_entangled(squbit) path_ancilla = self.new_ancilla() captured_ancilla = self.new_ancilla() captured_ancilla2 = self.new_ancilla() # capture e.p. has a special circuit self.circuit.append( qm.capture_ep(squbit, tqubit, epqubit, self.new_ancilla(), self.new_ancilla(), self.new_ancilla())) return 1 # Blocked/excluded e.p. post-select on the target if m.move_variant == enums.MoveVariant.EXCLUDED: is_there = self.post_select_on(tqubit) if is_there: return 0 self.add_entangled(tqubit) self.circuit.append( qm.en_passant(squbit, tqubit, epqubit, self.new_ancilla(), self.new_ancilla())) return 1 if m.move_type == enums.MoveType.PAWN_CAPTURE: # For pawn capture, first measure source. is_there = self.post_select_on(squbit) if not is_there: return 0 if tqubit in self.entangled_squares: old_tqubit = self.unhook(tqubit) self.add_entangled(squbit, tqubit) self.circuit.append( qm.controlled_operation(cirq.ISWAP, [squbit, tqubit], [old_tqubit], [])) else: # Classical case self.state = set_nth_bit(sbit, self.state, False) self.state = set_nth_bit(tbit, self.state, True) return 1 if m.move_type == enums.MoveType.SPLIT_SLIDE: tbit2 = square_to_bit(m.target2) tqubit2 = bit_to_qubit(tbit2) # Find all the squares on both paths path_qubits = self.path_qubits(m.source, m.target) path_qubits2 = self.path_qubits(m.source, m.target2) if len(path_qubits) == 0 and len(path_qubits2) == 0: # No interposing squares, just jump. m.move_type = enums.MoveType.SPLIT_JUMP else: self.add_entangled(squbit, tqubit, tqubit2) path1 = self.create_path_ancilla(path_qubits) path2 = self.create_path_ancilla(path_qubits2) ancilla = self.new_ancilla() self.circuit.append( qm.split_slide(squbit, tqubit, tqubit2, path1, path2, ancilla)) return 1 if m.move_type == enums.MoveType.MERGE_SLIDE: sbit2 = square_to_bit(m.source2) squbit2 = bit_to_qubit(sbit2) self.add_entangled(squbit, squbit2, tqubit) # Find all the squares on both paths path_qubits = self.path_qubits(m.source, m.target) path_qubits2 = self.path_qubits(m.source2, m.target) if len(path_qubits) == 0 and len(path_qubits2) == 0: # No interposing squares, just jump. m.move_type = enums.MoveType.MERGE_JUMP else: path1 = self.create_path_ancilla(path_qubits) path2 = self.create_path_ancilla(path_qubits2) ancilla = self.new_ancilla() self.circuit.append( qm.merge_slide(squbit, tqubit, squbit2, path1, path2, ancilla)) return 1 if (m.move_type == enums.MoveType.SLIDE or m.move_type == enums.MoveType.PAWN_TWO_STEP): path_qubits = self.path_qubits(m.source, m.target) if len(path_qubits) == 0: # No path, change to jump m.move_type = enums.MoveType.JUMP if (m.move_type == enums.MoveType.SLIDE or m.move_type == enums.MoveType.PAWN_TWO_STEP): for p in path_qubits: if (p not in self.entangled_squares and nth_bit_of(qubit_to_bit(p), self.state)): # Classical piece in the way return 0 # For excluded case, measure target if m.move_variant == enums.MoveVariant.EXCLUDED: is_there = self.post_select_on(tqubit) if is_there: return 0 self.add_entangled(squbit, tqubit) if m.move_variant == enums.MoveVariant.CAPTURE: capture_ancilla = self.new_ancilla() self.circuit.append( qm.controlled_operation(cirq.X, [capture_ancilla], [squbit], path_qubits)) # We need to add the captured_ancilla to entangled squares # So that we measure it self.entangled_squares.add(capture_ancilla) capture_allowed = self.post_select_on(capture_ancilla) if not capture_allowed: return 0 else: # Perform the captured slide self.add_entangled(squbit) # Remove the target from the board into an ancilla # and set bit to zero self.unhook(tqubit) self.state = set_nth_bit(tbit, self.state, False) # Re-add target since we need to swap into the square self.add_entangled(tqubit) # Perform the actual move self.circuit.append(qm.normal_move(squbit, tqubit)) # Set source to empty self.unhook(squbit) self.state = set_nth_bit(sbit, self.state, False) # Now set the whole path to empty for p in path_qubits: self.state = set_nth_bit(qubit_to_bit(p), self.state, False) self.unhook(p) return 1 # Basic slide (or successful excluded slide) # Add all involved squares into entanglement self.add_entangled(squbit, tqubit, *path_qubits) if len(path_qubits) == 1: # For path of one, no ancilla needed self.circuit.append(qm.slide_move(squbit, tqubit, path_qubits)) return 1 # Longer paths require a path ancilla ancilla = self.new_ancilla() self.circuit.append( qm.slide_move(squbit, tqubit, path_qubits, ancilla)) return 1 if (m.move_type == enums.MoveType.JUMP or m.move_type == enums.MoveType.PAWN_STEP): if (squbit not in self.entangled_squares and tqubit not in self.entangled_squares): # Classical version self.state = set_nth_bit(sbit, self.state, False) self.state = set_nth_bit(tbit, self.state, True) return 1 # Measure source for capture if m.move_variant == enums.MoveVariant.CAPTURE: is_there = self.post_select_on(squbit) if not is_there: return 0 self.unhook(tqubit) # Measure target for excluded if m.move_variant == enums.MoveVariant.EXCLUDED: is_there = self.post_select_on(tqubit) if is_there: return 0 # Only convert source qubit to ancilla if target # is empty unhook = tqubit not in self.entangled_squares self.add_entangled(squbit, tqubit) # Execute jump self.circuit.append(qm.normal_move(squbit, tqubit)) if unhook or m.move_variant != enums.MoveVariant.BASIC: # The source is empty. # Change source qubit to be an ancilla # and set classical bit to zero self.state = set_nth_bit(sbit, self.state, False) self.unhook(squbit) return 1 if m.move_type == enums.MoveType.SPLIT_JUMP: tbit2 = square_to_bit(m.target2) tqubit2 = bit_to_qubit(tbit2) self.add_entangled(squbit, tqubit, tqubit2) self.circuit.append(qm.split_move(squbit, tqubit, tqubit2)) self.state = set_nth_bit(sbit, self.state, False) self.unhook(squbit) return 1 if m.move_type == enums.MoveType.MERGE_JUMP: sbit2 = square_to_bit(m.source2) squbit2 = bit_to_qubit(sbit2) self.add_entangled(squbit, squbit2, tqubit) self.circuit.append(qm.merge_move(squbit, squbit2, tqubit)) # TODO: should the source qubit be 'unhooked'? return 1 if m.move_type == enums.MoveType.KS_CASTLE: # Figure out the rook squares if sbit == square_to_bit('e1') and tbit == square_to_bit('g1'): rook_sbit = square_to_bit('h1') rook_tbit = square_to_bit('f1') elif sbit == square_to_bit('e8') and tbit == square_to_bit('g8'): rook_sbit = square_to_bit('h8') rook_tbit = square_to_bit('f8') else: raise ValueError(f'Invalid kingside castling move') rook_squbit = bit_to_qubit(rook_sbit) rook_tqubit = bit_to_qubit(rook_tbit) # Piece in non-superposition in the way, not legal if (nth_bit_of(rook_tbit, self.state) and rook_tqubit not in self.entangled_squares): return 0 if (nth_bit_of(tbit, self.state) and tqubit not in self.entangled_squares): return 0 # Not in superposition, just castle if (rook_tqubit not in self.entangled_squares and tqubit not in self.entangled_squares): self.set_castle(sbit, rook_sbit, tbit, rook_tbit) return 1 # Both intervening squares in superposition if (rook_tqubit in self.entangled_squares and tqubit in self.entangled_squares): castle_ancilla = self.create_path_ancilla( [rook_tqubit, tqubit]) self.entangled_squares.add(castle_ancilla) castle_allowed = self.post_select_on(castle_ancilla) if castle_allowed: self.unhook(rook_tqubit) self.unhook(tqubit) self.set_castle(sbit, rook_sbit, tbit, rook_tbit) return 1 else: self.post_selection[castle_ancilla] = castle_allowed return 0 # One intervening square in superposition if rook_tqubit in self.entangled_squares: measure_qubit = rook_tqubit measure_bit = rook_tbit else: measure_qubit = tqubit measure_bit = tbit is_there = self.post_select_on(measure_qubit) if is_there: return 0 self.set_castle(sbit, rook_sbit, tbit, rook_tbit) return 1 if m.move_type == enums.MoveType.QS_CASTLE: # Figure out the rook squares and the b-file square involved if sbit == square_to_bit('e1') and tbit == square_to_bit('c1'): rook_sbit = square_to_bit('a1') rook_tbit = square_to_bit('d1') b_bit = square_to_bit('b1') elif sbit == square_to_bit('e8') and tbit == square_to_bit('c8'): rook_sbit = square_to_bit('a8') rook_tbit = square_to_bit('d8') b_bit = square_to_bit('b8') else: raise ValueError(f'Invalid queenside castling move') rook_squbit = bit_to_qubit(rook_sbit) rook_tqubit = bit_to_qubit(rook_tbit) b_qubit = bit_to_qubit(b_bit) # Piece in non-superposition in the way, not legal if (nth_bit_of(rook_tbit, self.state) and rook_tqubit not in self.entangled_squares): return 0 if (nth_bit_of(tbit, self.state) and tqubit not in self.entangled_squares): return 0 if (b_bit is not None and nth_bit_of(b_bit, self.state) and b_qubit not in self.entangled_squares): return 0 # Not in superposition, just castle if (rook_tqubit not in self.entangled_squares and tqubit not in self.entangled_squares and b_qubit not in self.entangled_squares): self.set_castle(sbit, rook_sbit, tbit, rook_tbit) return 1 # Neither intervening squares in superposition if (rook_tqubit not in self.entangled_squares and tqubit not in self.entangled_squares): if b_qubit not in self.entangled_squares: self.set_castle(sbit, rook_sbit, tbit, rook_tbit) else: self.queenside_castle(squbit, rook_squbit, tqubit, rook_tqubit, b_qubit) return 1 # Both intervening squares in superposition if (rook_tqubit in self.entangled_squares and tqubit in self.entangled_squares): castle_ancilla = self.create_path_ancilla( [rook_tqubit, tqubit]) self.entangled_squares.add(castle_ancilla) castle_allowed = self.post_select_on(castle_ancilla) if castle_allowed: self.unhook(rook_tqubit) self.unhook(tqubit) if b_qubit not in self.entangled_squares: self.set_castle(sbit, rook_sbit, tbit, rook_tbit) else: self.queenside_castle(squbit, rook_squbit, tqubit, rook_tqubit, b_qubit) return 1 else: self.post_selection[castle_ancilla] = castle_allowed return 0 # One intervening square in superposition if rook_tqubit in self.entangled_squares: measure_qubit = rook_tqubit measure_bit = rook_tbit else: measure_qubit = tqubit measure_bit = tbit is_there = self.post_select_on(measure_qubit) if is_there: return 0 if b_qubit not in self.entangled_squares: self.set_castle(sbit, rook_sbit, tbit, rook_tbit) else: self.queenside_castle(squbit, rook_squbit, tqubit, rook_tqubit, b_qubit) return 1 raise ValueError(f'Move type {m.move_type} not supported')
def _apply_cache(probability, m, cache_value): new_probability = probability.copy() for k, v in cache_value.items(): square = getattr(m, k) new_probability[square_to_bit(square)] = v return new_probability