Esempio n. 1
0
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)
Esempio n. 2
0
    def _caching_supported(self, m: move.Move):
        """Checks if caching is supported for this move."""

        # Caching is supported for a split jump from one full square to two empty squares.
        if (m.move_type == enums.MoveType.SPLIT_JUMP
                and nth_bit_of(square_to_bit(m.source), self.full_squares)
                and nth_bit_of(square_to_bit(m.target), self.empty_squares)
                and nth_bit_of(square_to_bit(m.target2), self.empty_squares)):
            return True
        return False
Esempio n. 3
0
    def path_qubits(self, source: str, target: str) -> List[cirq.Qid]:
        """Returns all entangled qubits (or classical pieces)
        between source and target.

        Source and target should be specified in algebraic notation,
        such as 'f4'.
        """
        rtn = []
        xs = move.x_of(source)
        ys = move.y_of(source)
        xt = move.x_of(target)
        yt = move.y_of(target)
        if xt > xs:
            dx = 1
        elif xt < xs:
            dx = -1
        else:
            dx = 0
        if yt > ys:
            dy = 1
        elif yt < ys:
            dy = -1
        else:
            dy = 0
        max_slide = max(abs(xs - xt), abs(ys - yt))
        if max_slide > 1:
            for t in range(1, max_slide):
                path_bit = xy_to_bit(xs + dx * t, ys + dy * t)
                path_qubit = bit_to_qubit(path_bit)
                if (path_qubit in self.entangled_squares
                        or nth_bit_of(path_bit, self.state)):
                    rtn.append(path_qubit)
        return rtn
Esempio n. 4
0
    def with_state(self, basis_state: int) -> "CirqBoard":
        """Resets the board with a specific classical state."""
        self.accumulations_repetitions = None
        self.board_accumulations_repetitions = None
        self.state = basis_state
        self.allowed_pieces = set()
        self.allowed_pieces.add(num_ones(self.state))
        self.entangled_squares = set()
        self.post_selection = {}
        self.circuit = cirq.Circuit()
        self.ancilla_count = 0
        self.move_history = []
        self.full_squares = basis_state
        self.empty_squares = 0
        for i in range(64):
            self.empty_squares = set_nth_bit(
                i, self.empty_squares, not nth_bit_of(i, self.full_squares))
        # Each entry is a 2-tuple of (repetitions, probabilities) corresponding to the probabilities after each move.
        self.move_history_probabilities_cache = []

        # Store the initial basis state so that we can use it for replaying
        # the move-history when undoing moves
        self.init_basis_state = basis_state
        self.clear_debug_log()
        self.timing_stats = defaultdict(list)
        return self
Esempio n. 5
0
def test_nth_bit_of():
    assert u.nth_bit_of(0, 7)
    assert u.nth_bit_of(1, 7)
    assert u.nth_bit_of(2, 7)
    assert not u.nth_bit_of(3, 7)
    assert not u.nth_bit_of(0, 6)
    assert u.nth_bit_of(1, 6)
    assert u.nth_bit_of(2, 6)
    assert not u.nth_bit_of(3, 6)
Esempio n. 6
0
    def add_entangled(self, *qubits):
        """Adds squares as entangled.

        This enables measuring of the square by the quantum circuit
        and also adds a piece in the square to the circuit if the
        classical register is currently set to one.
        """
        for qubit in qubits:
            if qubit not in self.entangled_squares:
                self.entangled_squares.add(qubit)
                if nth_bit_of(qubit_to_bit(qubit), self.state):
                    self.circuit.append(qm.place_piece(qubit))
Esempio n. 7
0
    def _generate_accumulations(self,
                                repetitions: int = 1000,
                                use_cache: bool = False) -> None:
        """Samples the state and generates the accumulated
        probabilities of each square, empty_squares, and full_squares.
        """

        if use_cache and self.move_history:
            last_move = self.move_history[-1]
            if self.move_history_probabilities_cache[-1][0] >= repetitions:
                self.probabilities = self.move_history_probabilities_cache[-1][
                    1].copy()
                self._set_full_empty_squares_from_probability()
                return
            previous_move_in_cache = (
                len(self.move_history) > 1 and
                self.move_history_probabilities_cache[-2][0] >= repetitions)
            is_first_move = len(self.move_history) == 1
            cache_key = cache_key_from_move(last_move, repetitions)
            if ((previous_move_in_cache or is_first_move)
                    and self._caching_supported(last_move)
                    and cache_key in self.cache):
                if previous_move_in_cache:
                    previous_probability = self.move_history_probabilities_cache[
                        len(self.move_history) - 2][1]
                else:
                    previous_probability = [0] * 64
                    for i in range(64):
                        # Assume initial state is classical
                        previous_probability[i] = nth_bit_of(i, self.state)

                probs = self._apply_cache(previous_probability, last_move,
                                          self.cache[cache_key])
                self.probabilities = probs
                self._set_full_empty_squares_from_probability()
                self.move_history_probabilities_cache.append(
                    (repetitions, probs.copy()))
                # Remove entry from cached since it has been consumed.
                del self.cache[cache_key]
                return

        self.probabilities = [0] * 64
        samples = self.sample(repetitions)
        for sample in samples:
            for bit in bit_ones(sample):
                self.probabilities[bit] += 1

        for bit in range(64):
            self.probabilities[bit] = float(
                self.probabilities[bit]) / float(repetitions)
        self._set_full_empty_squares_from_probability()

        self.accumulations_repetitions = repetitions
Esempio n. 8
0
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)
Esempio n. 9
0
    def get_probability_distribution(self,
                                     repetitions: int = 1000) -> List[float]:
        """Returns the probability of a piece being in each square.

        The values are returned as a list in the same ordering as a
        bitboard.
        """
        samples = self.sample(repetitions)
        counts = [0] * 64
        for sample in samples:
            for bit in range(64):
                if nth_bit_of(bit, sample):
                    counts[bit] += 1
        for bit in range(64):
            counts[bit] = float(counts[bit]) / float(repetitions)
        return counts
Esempio n. 10
0
    def get_empty_squares_bitboard(self, repetitions: int = 1000) -> int:
        """Retrieves which squares are marked as full.

        This information is created using a representative set of
        samples (defined by the repetitions argument) to determine
        which squares are empty on all boards.

        Returns a bitboard.
        """
        samples = self.sample(repetitions)
        empty_squares = (1 << 64) - 1
        for bit in range(64):
            for sample in samples:
                if nth_bit_of(bit, sample):
                    empty_squares = set_nth_bit(bit, empty_squares, 0)
                    break
        return empty_squares
Esempio n. 11
0
    def get_full_squares_bitboard(self, repetitions: int = 1000) -> int:
        """Retrieves which squares are marked as full.

        This information is created using a representative set of
        samples (defined by the repetitions argument) to determine
        which squares are occupied on at least one board.

        Returns a bitboard.
        """
        samples = self.sample(repetitions)
        full_squares = 0
        for bit in range(64):
            for sample in samples:
                if nth_bit_of(bit, sample):
                    full_squares = set_nth_bit(bit, full_squares, 1)
                    break
        return full_squares
Esempio n. 12
0
    def post_select_on(
        self,
        qubit: cirq.Qid,
        measurement_outcome: Optional[int] = None,
        invert: Optional[bool] = False,
    ) -> bool:
        """Adds a post-selection requirement to the circuit.

        If no measurement_outcome is provided, performs a single sample of the
        qubit to get a value for the post-selection condition.
        Adjusts the post-selection requirements dictionary to this value.
        If this qubit is a square qubit, it adjusts the classical register
        to match the sample result.

        Args:
            qubit: the qubit to post-select on
            measurement_outcome: the optional measurement outcome. If present,
                post-selection is conditioned on the qubit having the given
                outcome. If absent, a single measurement is performed instead.
            invert: If True and measurement_outcome is set, this will invert
                the measurement to post-select on the opposite value.

        Returns: the measurement outcome or sample result as 1 or 0.
        """
        result = measurement_outcome
        if invert and measurement_outcome is not None:
            result = 1 - result
        sample_size = 100 if self.noise_mitigation else 1
        if "anc" in qubit.name:
            if result is None:
                ancilla_result = []
                while len(ancilla_result) == 0:
                    _, ancilla_result = self.sample_with_ancilla(sample_size)
                result = ancilla_result[0][qubit.name]
            self.post_selection[qubit] = result
        else:
            bit = qubit_to_bit(qubit)
            if result is None:
                result = nth_bit_of(bit, self.sample(sample_size)[0])
            if qubit in self.entangled_squares:
                ancillary = self.unhook(qubit)
                self.post_selection[ancillary] = result
            self.state = set_nth_bit(bit, self.state, result)
        return result
Esempio n. 13
0
    def path_qubits(self, source: str, target: str) -> List[cirq.Qid]:
        """Returns all entangled qubits (or classical pieces)
        between source and target.

        Source and target should be in the same line, i.e. same row, 
        same column, or same diagonal.

        Source and target should be specified in algebraic notation,
        such as 'f4'.
        """
        rtn = []
        xs = move.x_of(source)
        ys = move.y_of(source)
        xt = move.x_of(target)
        yt = move.y_of(target)
        if xt > xs:
            dx = 1
        elif xt < xs:
            dx = -1
        else:
            dx = 0
        if yt > ys:
            dy = 1
        elif yt < ys:
            dy = -1
        else:
            dy = 0
        x_slide = abs(xt - xs)
        y_slide = abs(yt - ys)
        # Souce and target should always be in the same line.
        if x_slide != y_slide and x_slide * y_slide:
            raise ValueError(
                'Wrong inputs for path_qubits: source and target are not in the same line.'
            )
        max_slide = max(x_slide, y_slide)
        # Only calculates path when max_slide > 1.
        for t in range(1, max_slide):
            path_bit = xy_to_bit(xs + dx * t, ys + dy * t)
            path_qubit = bit_to_qubit(path_bit)
            if (path_qubit in self.entangled_squares
                    or nth_bit_of(path_bit, self.state)):
                rtn.append(path_qubit)
        return rtn
Esempio n. 14
0
    def _generate_accumulations(self, repetitions: int = 1000) -> None:
        """ Samples the state and generates the accumulated 
        probabilities, empty_squares, and full_squares
        """
        self.probabilities = [0] * 64
        self.full_squares = (1 << 64) - 1
        self.empty_squares = (1 << 64) - 1

        samples = self.sample(repetitions)
        for sample in samples:
            for bit in range(64):
                if nth_bit_of(bit, sample):
                    self.probabilities[bit] += 1
                    self.empty_squares = set_nth_bit(bit, self.empty_squares,
                                                     0)
                else:
                    self.full_squares = set_nth_bit(bit, self.full_squares, 0)

        for bit in range(64):
            self.probabilities[bit] = float(
                self.probabilities[bit]) / float(repetitions)

        self.accumulations_valid = True
Esempio n. 15
0
    def post_select_on(self, qubit: cirq.Qid) -> int:
        """Adds a post-selection requirement to the circuit,

        Performs a single sample of the qubit to get a value.
        Adjusts the post-selection requirements dictionary to this value.
        If this qubit is a square qubit, it adjusts the classical register
        to match the sample result.

        Returns: the sample result as 1 or 0.
        """
        if 'anc' in qubit.name:
            ancilla_result = []
            while len(ancilla_result) == 0:
                _, ancilla_result = self.sample_with_ancilla(10)
            result = ancilla_result[0][qubit.name]
            self.post_selection[qubit] = result
        else:
            bit = qubit_to_bit(qubit)
            result = nth_bit_of(bit, self.sample(1)[0])
            if qubit in self.entangled_squares:
                ancillary = self.unhook(qubit)
                self.post_selection[ancillary] = result
            self.state = set_nth_bit(bit, self.state, result)
        return result
Esempio n. 16
0
    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')