예제 #1
0
def test_sampler_expectation_value() -> None:
    c = Circuit(2)
    c.H(0)
    c.CX(0, 1)
    op = QubitPauliOperator({
        QubitPauliString({
            Qubit(0): Pauli.Z,
            Qubit(1): Pauli.Z
        }):
        1.0,
        QubitPauliString({
            Qubit(0): Pauli.X,
            Qubit(1): Pauli.X
        }):
        0.3,
        QubitPauliString({
            Qubit(0): Pauli.Z,
            Qubit(1): Pauli.Y
        }):
        0.8j,
        QubitPauliString({Qubit(0): Pauli.Y}):
        -0.4j,
    })
    b = MySampler()
    b.compile_circuit(c)
    expectation = get_operator_expectation_value(c,
                                                 op,
                                                 b,
                                                 n_shots=2000,
                                                 seed=0)
    assert (np.real(expectation), np.imag(expectation)) == pytest.approx(
        (1.3, 0.0), abs=0.1)
예제 #2
0
def test_expectation_value() -> None:
    c = Circuit(2)
    c.H(0)
    c.CX(0, 1)
    op = QubitPauliOperator({
        QubitPauliString({
            Qubit(0): Pauli.Z,
            Qubit(1): Pauli.Z
        }):
        1.0,
        QubitPauliString({
            Qubit(0): Pauli.X,
            Qubit(1): Pauli.X
        }):
        0.3,
        QubitPauliString({
            Qubit(0): Pauli.Z,
            Qubit(1): Pauli.Y
        }):
        0.8j,
        QubitPauliString({Qubit(0): Pauli.Y}):
        -0.4j,
    })
    b = MyBackend()
    b.compile_circuit(c)
    assert get_operator_expectation_value(c, op, b) == pytest.approx(1.3)
예제 #3
0
def tk_to_mycircuit(tkc: Circuit) -> MyCircuit:
    """Convert a pytket Circuit to a MyCircuit object.
    Supports Rz, Rx, Ry, and ZZMax gates.

    :param tkc: The Circuit to convert
    :type tkc: Circuit
    :return: An equivalent MyCircuit object
    :rtype: MyCircuit
    """
    circ = MyCircuit(tkc.qubits)
    for command in tkc:
        optype = command.op.type
        if optype == OpType.Rx:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.X),
                          np.pi * command.op.params[0])
        elif optype == OpType.Ry:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.Y),
                          np.pi * command.op.params[0])
        elif optype == OpType.Rz:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.Z),
                          np.pi * command.op.params[0])
        elif optype == OpType.ZZMax:
            circ.add_gate(QubitPauliString(command.args, [Pauli.Z, Pauli.Z]),
                          np.pi * 0.5)
        else:
            raise ValueError("Cannot convert optype to MyCircuit: ", optype)
    return circ
예제 #4
0
def test_aer_placed_expectation() -> None:
    # bug TKET-695
    n_qbs = 3
    c = Circuit(n_qbs, n_qbs)
    c.X(0)
    c.CX(0, 2)
    c.CX(1, 2)
    c.H(1)
    # c.measure_all()
    b = AerBackend()
    operator = QubitPauliOperator({
        QubitPauliString(Qubit(0), Pauli.Z): 1.0,
        QubitPauliString(Qubit(1), Pauli.X): 0.5,
    })
    assert b.get_operator_expectation_value(c, operator) == (-0.5 + 0j)
    with open(os.path.join(sys.path[0], "ibmqx2_properties.pickle"),
              "rb") as f:
        properties = pickle.load(f)

    noise_model = NoiseModel.from_backend(properties)

    noise_b = AerBackend(noise_model)

    with pytest.raises(RuntimeError) as errorinfo:
        noise_b.get_operator_expectation_value(c, operator)
        assert "not supported with noise model" in str(errorinfo.value)

    c.rename_units({Qubit(1): Qubit("node", 1)})
    with pytest.raises(ValueError) as errorinfoCirc:
        b.get_operator_expectation_value(c, operator)
        assert "default register Qubits" in str(errorinfoCirc.value)
def test_hadamard() -> None:
    c = MyCircuit([Qubit(0), Qubit(1)], [Bit(0), Bit(1)])
    c.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)
    c.add_measure(Qubit(0), Bit(0))
    counts = get_counts(c, n_shots=100, seed=11)
    assert counts == {(0, 0): 50, (0, 1): 50}
def test_basic_ordering() -> None:
    # Test that final bit readouts are in the intended order (DLO)
    c = MyCircuit([Qubit(0)], [Bit(0), Bit(1), Bit(2)])
    c.add_measure(Qubit(0), Bit(0))
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi)
    c.add_measure(Qubit(0), Bit(2))
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_measure(Qubit(0), Bit(1))
    counts = get_counts(c, n_shots=100, seed=11)
    assert counts == {(1, 0, 0): 50, (1, 1, 0): 50}
예제 #7
0
def test_operator_statevector(qvm: None, quilc: None) -> None:
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    b = ForestStateBackend()
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    iz = QubitPauliString(Qubit(1), Pauli.Z)
    op = QubitPauliOperator({zi: 0.3, iz: -0.1})
    assert get_operator_expectation_value(c, op, b) == pytest.approx(0.2)
    c.X(0)
    assert get_operator_expectation_value(c, op, b) == pytest.approx(-0.4)
def test_overwrite() -> None:
    # Test that classical data is overwritten by later measurements appropriately
    c = MyCircuit([Qubit(0)], [Bit(0), Bit(1)])
    c.add_measure(Qubit(0), Bit(0))
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi)
    c.add_measure(Qubit(0), Bit(0))
    c.add_measure(Qubit(0), Bit(1))
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_measure(Qubit(0), Bit(1))
    counts = get_counts(c, n_shots=100, seed=11)
    assert counts == {(0, 1): 50, (1, 1): 50}
def test_conditional_rotation() -> None:
    c = MyCircuit([Qubit(0)], [Bit(0), Bit(1)])
    # Randomise qubit state
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_measure(Qubit(0), Bit(0))
    # Correct qubit state to |1> - this should only happen when the measurement was 0
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi, {Bit(0): 0, Bit(1): 0})
    # Randomise final measurement - this should never happen
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2, {Bit(0): 0, Bit(1): 1})
    c.add_measure(Qubit(0), Bit(1))
    counts = get_counts(c, n_shots=100, seed=11)
    assert counts == {(1, 0): 50, (1, 1): 50}
예제 #10
0
def test_operator_sim(qvm: None, quilc: None) -> None:
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    b = ForestBackend("9q-square")
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    iz = QubitPauliString(Qubit(1), Pauli.Z)
    op = QubitPauliOperator({zi: 0.3, iz: -0.1})
    assert get_operator_expectation_value(c, op, b,
                                          10) == pytest.approx(0.2, rel=0.001)
    c.X(0)
    assert get_operator_expectation_value(c, op, b,
                                          10) == pytest.approx(-0.4, rel=0.001)
예제 #11
0
def test_expectation() -> None:
    b = BraketBackend(local=True)
    assert b.supports_expectation
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    iz = QubitPauliString(Qubit(1), Pauli.Z)
    op = QubitPauliOperator({zi: 0.3, iz: -0.1})
    assert get_pauli_expectation_value(c, zi, b) == 1
    assert get_operator_expectation_value(c, op, b) == pytest.approx(0.2)
    c.X(0)
    assert get_pauli_expectation_value(c, zi, b) == -1
    assert get_operator_expectation_value(c, op, b) == pytest.approx(-0.4)
예제 #12
0
def test_simulator() -> None:
    b = BraketBackend(
        s3_bucket=S3_BUCKET,
        s3_folder=S3_FOLDER,
        device_type="quantum-simulator",
        provider="amazon",
        device="sv1",
    )
    assert b.supports_shots
    c = Circuit(2).H(0).CX(0, 1)
    b.compile_circuit(c)
    n_shots = 100
    h0, h1 = b.process_circuits([c, c], n_shots)
    res0 = b.get_result(h0)
    readouts = res0.get_shots()
    assert all(readouts[i][0] == readouts[i][1] for i in range(n_shots))
    res1 = b.get_result(h1)
    counts = res1.get_counts()
    assert len(counts) <= 2
    assert sum(counts.values()) == n_shots
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    assert b.get_pauli_expectation_value(
        c, zi, poll_timeout_seconds=60, poll_interval_seconds=1
    ) == pytest.approx(0)

    # Circuit with unused qubits
    c = Circuit(3).H(1).CX(1, 2)
    b.compile_circuit(c)
    h = b.process_circuit(c, 1)
    res = b.get_result(h)
    readout = res.get_shots()[0]
    assert readout[1] == readout[2]
예제 #13
0
def qps_from_openfermion(paulis):
    # translate from openfermion format to a QubitPauliString
    qlist = []
    plist = []
    for q, p in paulis:
        qlist.append(Qubit(q))
        plist.append(pauli_sym[p])
    return QubitPauliString(qlist, plist)
예제 #14
0
def test_device() -> None:
    c = circuit_gen(False)
    b = IBMQBackend("ibmq_santiago", hub="ibm-q", group="open", project="main")
    assert b._max_per_job == 75
    operator = QubitPauliOperator({
        QubitPauliString(Qubit(0), Pauli.Z): 1.0,
        QubitPauliString(Qubit(0), Pauli.X): 0.5,
    })
    val = get_operator_expectation_value(c, operator, b, 8000)
    print(val)
    c1 = circuit_gen(True)
    c2 = circuit_gen(True)
    b.compile_circuit(c1)
    b.compile_circuit(c2)

    print(b.get_shots(c1, n_shots=10))
    print(b.get_shots(c2, n_shots=10))
def test_operator() -> None:
    c = circuit_gen()
    b = ProjectQBackend()
    zz = QubitPauliOperator(
        {QubitPauliString([Qubit(0), Qubit(1)], [Pauli.Z, Pauli.Z]): 1.0})
    assert np.isclose(get_operator_expectation_value(c, zz, b), complex(1.0))
    c.X(0)
    assert np.isclose(get_operator_expectation_value(c, zz, b), complex(-1.0))
예제 #16
0
def test_pauli_statevector(qvm: None, quilc: None) -> None:
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    b = ForestStateBackend()
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    assert get_pauli_expectation_value(c, zi, b) == 1
    c.X(0)
    assert get_pauli_expectation_value(c, zi, b) == -1
예제 #17
0
def test_operator() -> None:
    for b in [AerBackend(), AerStateBackend()]:
        c = circuit_gen()
        zz = QubitPauliOperator(
            {QubitPauliString([Qubit(0), Qubit(1)], [Pauli.Z, Pauli.Z]): 1.0})
        assert cmath.isclose(get_operator_expectation_value(c, zz, b), 1.0)
        c.X(0)
        assert cmath.isclose(get_operator_expectation_value(c, zz, b), -1.0)
def test_pauli() -> None:
    c = Circuit(2)
    c.Rz(0.5, 0)
    b = ProjectQBackend()
    zi = QubitPauliString({Qubit(0): Pauli.Z})
    assert np.isclose(get_pauli_expectation_value(c, zi, b), complex(1))
    c.X(0)
    assert np.isclose(get_pauli_expectation_value(c, zi, b), complex(-1))
def qps_from_openfermion(paulis: QubitOperator) -> QubitPauliString:
    """ Utility function to translate from openfermion format to a QubitPauliString """
    qlist = []
    plist = []
    for q, p in paulis:
        qlist.append(Qubit(q))
        plist.append(pauli_sym[p])
    return QubitPauliString(qlist, plist)
예제 #20
0
def test_pauli_statevector() -> None:
    c = Circuit(2)
    c.Rz(0.5, 0)
    Transform.OptimisePostRouting().apply(c)
    b = AerStateBackend()
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    assert get_pauli_expectation_value(c, zi, b) == 1
    c.X(0)
    assert get_pauli_expectation_value(c, zi, b) == -1
예제 #21
0
def test_pauli() -> None:
    for b in [AerBackend(), AerStateBackend()]:
        c = Circuit(2)
        c.Rz(0.5, 0)
        b.compile_circuit(c)
        zi = QubitPauliString(Qubit(0), Pauli.Z)
        assert cmath.isclose(get_pauli_expectation_value(c, zi, b), 1)
        c.X(0)
        assert cmath.isclose(get_pauli_expectation_value(c, zi, b), -1)
예제 #22
0
def test_pauli_sim(qvm: None, quilc: None) -> None:
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    b = ForestBackend("9q-square")
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    energy = get_pauli_expectation_value(c, zi, b, 10)
    assert abs(energy - 1) < 0.001
    c.X(0)
    energy = get_pauli_expectation_value(c, zi, b, 10)
    assert abs(energy + 1) < 0.001
예제 #23
0
def test_pauli_sim() -> None:
    c = Circuit(2, 2)
    c.Rz(0.5, 0)
    Transform.OptimisePostRouting().apply(c)
    b = AerBackend()
    zi = QubitPauliString(Qubit(0), Pauli.Z)
    energy = get_pauli_expectation_value(c, zi, b, 8000)
    assert abs(energy - 1) < 0.001
    c.X(0)
    energy = get_pauli_expectation_value(c, zi, b, 8000)
    assert abs(energy + 1) < 0.001
def test_conditional_measurement() -> None:
    c = MyCircuit([Qubit(0), Qubit(1)], [Bit(0), Bit(1), Bit(2), Bit(3)])
    # Get a random number
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_measure(Qubit(0), Bit(0))
    # If random number is 0 then flip to give |1>
    # Otherwise, randomly generate |+i> or |-i>
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_measure(Qubit(0), Bit(1), {Bit(0): 1})
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    # Deterministic if Bit(0) == 0, random if Bit(0) == 1
    c.add_measure(Qubit(0), Bit(2))
    # Test end-of-circuit conditions by copying Bit(2) to Bit(3)
    c.add_gate(QubitPauliString(Qubit(1), Pauli.X), np.pi)
    c.add_measure(Qubit(1), Bit(3), {Bit(2): 1})
    counts = get_counts(c, n_shots=10000, seed=11)
    assert counts[(1, 1, 0, 0)] == pytest.approx(5000, rel=0.02)
    assert counts[(0, 0, 0, 1)] == pytest.approx(1250, rel=0.02)
    assert counts[(1, 1, 0, 1)] == pytest.approx(1250, rel=0.02)
    assert counts[(0, 0, 1, 1)] == pytest.approx(1250, rel=0.02)
    assert counts[(1, 1, 1, 1)] == pytest.approx(1250, rel=0.02)
예제 #25
0
def test_variance() -> None:
    b = BraketBackend(local=True)
    assert b.supports_variance
    # - Prepare a state (1/sqrt(2), 1/sqrt(2)).
    # - Measure w.r.t. the operator Z which has evcs (1,0) (evl=+1) and (0,1) (evl=-1).
    # - Get +1 with prob. 1/2 and -1 with prob. 1/2.
    c = Circuit(1).H(0)
    z = QubitPauliString(Qubit(0), Pauli.Z)
    assert b.get_pauli_expectation_value(c, z) == pytest.approx(0)
    assert b.get_pauli_variance(c, z) == pytest.approx(1)
    op = QubitPauliOperator({z: 3})
    assert b.get_operator_expectation_value(c, op) == pytest.approx(0)
    assert b.get_operator_variance(c, op) == pytest.approx(9)
예제 #26
0
def test_moments_with_shots() -> None:
    b = BraketBackend(local=True)
    c = Circuit(1).H(0)
    z = QubitPauliString(Qubit(0), Pauli.Z)
    e = b.get_pauli_expectation_value(c, z, n_shots=10)
    assert abs(e) <= 1
    v = b.get_pauli_variance(c, z, n_shots=10)
    assert v <= 1
    op = QubitPauliOperator({z: 3})
    e = b.get_operator_expectation_value(c, op, n_shots=10)
    assert abs(e) <= 3
    v = b.get_operator_variance(c, op, n_shots=10)
    assert v <= 9
예제 #27
0
def tk_to_mymeasures(tkc: Circuit) -> Tuple[MyCircuit, Dict[Qubit, Bit]]:
    """Convert a pytket Circuit to a MyCircuit object and a measurement map.
    Supports Rz, Rx, Ry, and ZZMax gates, as well as end-of-circuit measurements.

    :param tkc: The Circuit to convert
    :type tkc: Circuit
    :return: An equivalent MyCircuit object and a map from measured Qubit to the Bit containing the result
    :rtype: Tuple[MyCircuit, Dict[Qubit, Bit]]
    """
    circ = MyCircuit(tkc.qubits)
    measure_map = dict()
    measured_units = (
        set()
    )  # Track measured Qubits/used Bits to identify mid-circuit measurement
    for command in tkc:
        for u in command.args:
            if u in measured_units:
                raise ValueError("Circuit contains a mid-circuit measurement")
        optype = command.op.type
        if optype == OpType.Rx:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.X),
                          np.pi * command.op.params[0])
        elif optype == OpType.Ry:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.Y),
                          np.pi * command.op.params[0])
        elif optype == OpType.Rz:
            circ.add_gate(QubitPauliString(command.args[0], Pauli.Z),
                          np.pi * command.op.params[0])
        elif optype == OpType.ZZMax:
            circ.add_gate(QubitPauliString(command.args, [Pauli.Z, Pauli.Z]),
                          np.pi * 0.5)
        elif optype == OpType.Measure:
            measure_map[command.args[0]] = command.args[1]
            measured_units.add(command.args[0])
            measured_units.add(command.args[1])
        else:
            raise ValueError("Cannot convert optype to MyCircuit: ", optype)
    return circ, measure_map
def test_bell() -> None:
    c = MyCircuit([Qubit(0), Qubit(1)], [Bit(0), Bit(1)])
    # Hadamard Q0
    c.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)
    c.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)
    c.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)
    # CX Q0 Q1
    c.add_gate(QubitPauliString(Qubit(0), Pauli.Z), -np.pi / 2)
    c.add_gate(QubitPauliString(Qubit(1), Pauli.X), -np.pi / 2)
    c.add_gate(QubitPauliString([Qubit(0), Qubit(1)], [Pauli.Z, Pauli.X]), np.pi / 2)
    c.add_measure(Qubit(0), Bit(0))
    c.add_measure(Qubit(1), Bit(1))
    counts = get_counts(c, n_shots=100, seed=11)
    assert counts == {(0, 0): 50, (1, 1): 50}
from pytket.utils import QubitPauliOperator
from pytket.utils.expectations import get_operator_expectation_value
from pytket.extensions.qiskit import AerBackend, AerStateBackend

# First, let's get some results on a toy circuit without using any measurement reduction:

shots_backend = AerBackend()
n_shots = 10000

c = Circuit(5)
c.H(4)
c.V(2)

shots_backend.compile_circuit(c)
op = QubitPauliOperator({
    QubitPauliString([Qubit(0)], [Pauli.Z]):
    0.1,
    QubitPauliString(
        [Qubit(0), Qubit(1), Qubit(2),
         Qubit(3), Qubit(4)],
        [Pauli.Y, Pauli.Z, Pauli.X, Pauli.X, Pauli.Y],
    ):
    0.4,
    QubitPauliString([Qubit(0), Qubit(1)], [Pauli.X, Pauli.X]):
    0.2,
})

shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots)
print(shots_result)

# The result should be around 0.1, although as the shot simulator is stochastic this will be inexact. Let's test to check what the exact result should be using the statevector simulator:
    def sample(self, n_shots: int, seed: Optional[int] = None) -> np.ndarray:
        """Sample from the final classical distribution.
        For each sample, will pick a branch for each internal measurement and traverse
        the simulation tree until the end-of-circuit measurements are reached.
        The tree caches the state of the simulation for each branch to reuse for later shots.

        :param n_shots: The number of samples to take
        :type n_shots: int
        :param seed: Seed for the random sampling, defaults to None
        :type seed: Optional[int], optional
        :return: Shot table with each row corresponding to a shot. Columns correspond to Bits in
        decreasing lexicographical order, i.e. [0, 1] corresponds to {Bit(0) : 1, Bit(1) : 0}
        :rtype: np.ndarray
        """
        if seed is not None:
            np.random.seed(seed)
        table = np.zeros((n_shots, len(self.bits)), dtype=int)
        for s in range(n_shots):
            # Uniformly select a random point from the measurement distribution
            point = np.random.uniform(0.0, 1.0)

            # Traverse the tree until we reach the end-of-circuit measurements
            current_node = self.tree
            # The range of values `point` could take to end up in the current node
            current_lower = 0.0
            current_upper = 1.0
            while not isinstance(current_node.data, CompleteNode):
                if isinstance(current_node.data, IncompleteNode):
                    # When at an IncompleteNode, there are no future branches already considered
                    # but we still have computation to simulate on this branch
                    # Simulate new gates until either a mid-circuit measurement
                    # or we reach the end of the internal gates
                    while True:
                        try:
                            next_gate = next(current_node.data.gate_iter)
                        except StopIteration:
                            # There are no more internal gates, so cumulative probabilities for
                            # the final measurements of every qubit
                            cum_probs = (
                                current_node.data.qstate
                                * current_node.data.qstate.conjugate()
                            ).cumsum()
                            current_node.data = CompleteNode(
                                cum_probs, current_node.data.cstate
                            )
                            break

                        # Skip the gate if the classical condition is not met
                        if next_gate.condition:
                            condition_met = True
                            for b, v in next_gate.condition.items():
                                bi = self.bits.index(b)
                                if current_node.data.cstate[bi] != v:
                                    condition_met = False
                                    break
                            if not condition_met:
                                continue

                        if isinstance(next_gate, Rotation):
                            # Apply the rotation to the quantum state and continue to the next gate
                            pauli_tensor = next_gate.qps.to_sparse_matrix(self.qubits)
                            exponent = -0.5 * next_gate.angle
                            current_node.data.qstate = np.cos(
                                exponent
                            ) * current_node.data.qstate + 1j * np.sin(
                                exponent
                            ) * pauli_tensor.dot(
                                current_node.data.qstate
                            )
                        else:
                            # Otherwise, we have a measurement
                            # Compute the states after a 0-outcome and a 1-outcome and make a branch

                            # Project into measurement subspaces
                            identity = QubitPauliString().to_sparse_matrix(self.qubits)
                            z_op = QubitPauliString(
                                next_gate.qubit, Pauli.Z
                            ).to_sparse_matrix(self.qubits)
                            zero_proj = 0.5 * (identity + z_op)
                            one_proj = 0.5 * (identity - z_op)
                            zero_state = zero_proj.dot(current_node.data.qstate)
                            one_state = one_proj.dot(current_node.data.qstate)

                            # Find probability of measurement and normalise
                            zero_prob = np.vdot(zero_state, zero_state)
                            if zero_prob >= 1e-10:  # Prevent divide-by-zero errors
                                zero_state *= 1 / np.sqrt(zero_prob)
                            if 1 - zero_prob >= 1e-10:
                                one_state *= 1 / np.sqrt(1 - zero_prob)

                            # Update the classical state for each outcome
                            bit_index = self.bits.index(next_gate.bit)
                            zero_cstate = copy(current_node.data.cstate)
                            zero_cstate[bit_index] = 0
                            one_cstate = current_node.data.cstate
                            one_cstate[bit_index] = 1

                            # Replace current node in the tree by a branch, with each outcome as children
                            zero_node = Node(0)
                            zero_node.data = IncompleteNode(
                                zero_state,
                                zero_cstate,
                                copy(current_node.data.gate_iter),
                            )
                            one_node = Node(0)
                            one_node.data = IncompleteNode(
                                one_state, one_cstate, current_node.data.gate_iter
                            )
                            current_node.data = InternalNode(zero_prob)
                            current_node.left = zero_node
                            current_node.right = one_node
                            break
                else:
                    # Reached an internal measurement, so randomly pick a branch to traverse
                    current_decision = (
                        current_lower
                        + (current_upper - current_lower) * current_node.data.zero_prob
                    )
                    if point < current_decision:
                        current_node = current_node.left
                        current_upper = current_decision
                    else:
                        current_node = current_node.right
                        current_lower = current_decision
            # Finally reached the end of the circuit
            # Randomly sample from the final measurements
            index = np.searchsorted(
                current_node.data.cum_probs, point / (current_upper - current_lower)
            )
            bitstring = bin(index)[2:].zfill(len(self.qubits))

            # Update the classical state with final measurement outcomes
            table[s] = current_node.data.cstate
            for g in self.end_measures:
                if g.condition:
                    # If final measurements are conditioned, the classical state may not be updated
                    condition_met = True
                    for b, v in g.condition.items():
                        bi = self.bits.index(b)
                        if current_node.data.cstate[bi] != v:
                            condition_met = False
                            break
                    if not condition_met:
                        continue
                qi = self.qubits.index(g.qubit)
                bi = self.bits.index(g.bit)
                table[s, bi] = int(bitstring[qi])
        return table