|
| 1 | +"""Test RNG capabilities.""" |
| 2 | + |
| 3 | +from typing import Callable, ContextManager |
| 4 | + |
| 5 | +import numpy as np |
| 6 | +from pytket.backends.backendresult import BackendResult |
| 7 | +from pytket.circuit import Bit, Circuit |
| 8 | + |
| 9 | +import qnexus as qnx |
| 10 | +from qnexus.models.references import ( |
| 11 | + CircuitRef, |
| 12 | + ExecutionResultRef, |
| 13 | +) |
| 14 | + |
| 15 | + |
| 16 | +def get_rng_circuit(seed: int, n_rng: int, test_index: bool = False) -> Circuit: |
| 17 | + """Creates a single qubit pytket circuit to test RNGs. |
| 18 | +
|
| 19 | + Note: see https://docs.quantinuum.com/systems/trainings/h2/getting_started/rng.html |
| 20 | + and https://docs.quantinuum.com/tket/api-docs/circuit_class.html#rng-operations |
| 21 | + """ |
| 22 | + circuit = Circuit(1) |
| 23 | + |
| 24 | + rng_regs = [] |
| 25 | + for i in range(n_rng): |
| 26 | + rng_regs.append(circuit.add_c_register(f"rng{i}", 32)) |
| 27 | + |
| 28 | + seed_reg = circuit.add_c_register("seed", 64) |
| 29 | + circuit.add_c_setreg(seed, seed_reg) |
| 30 | + circuit.set_rng_seed(seed_reg) |
| 31 | + |
| 32 | + job_shot_reg = circuit.add_c_register("job_shot_num", 32) |
| 33 | + circuit.get_job_shot_num(job_shot_reg) |
| 34 | + |
| 35 | + if test_index: |
| 36 | + # set rng index = job_shotnum * num_rng_calls |
| 37 | + index_reg = circuit.add_c_register("index", 32) |
| 38 | + circuit.add_clexpr_from_logicexp(job_shot_reg * n_rng, index_reg.to_list()) |
| 39 | + circuit.set_rng_index(index_reg) |
| 40 | + |
| 41 | + for rng_reg in rng_regs: |
| 42 | + circuit.get_rng_num(rng_reg) |
| 43 | + circuit.measure_all() |
| 44 | + |
| 45 | + return circuit |
| 46 | + |
| 47 | + |
| 48 | +def ints_from_pytket_shots(shots: np.ndarray) -> list[int]: |
| 49 | + """Convert pytket register shots to integers. |
| 50 | +
|
| 51 | + Args: |
| 52 | + shots: Array of shape (n_shots, n_bits) from result.get_shots(cbits=reg). |
| 53 | + Column i corresponds to reg[i], with reg[0] as the LSB. |
| 54 | +
|
| 55 | + Returns: |
| 56 | + List of integers, one per shot. |
| 57 | + """ |
| 58 | + powers = 1 << np.arange(shots.shape[1]) |
| 59 | + return list(map(int, (shots @ powers).tolist())) |
| 60 | + |
| 61 | + |
| 62 | +def test_rng( |
| 63 | + test_case_name: str, |
| 64 | + create_circuit_in_project: Callable[ |
| 65 | + [Circuit, str, str], ContextManager[CircuitRef] |
| 66 | + ], |
| 67 | +) -> None: |
| 68 | + """Test that we can run RNG circuits in H-Series machines.""" |
| 69 | + local_project_name = f"project for {test_case_name}" |
| 70 | + backend_config = qnx.QuantinuumConfig(device_name="H2-1E") |
| 71 | + n_shots = 5 |
| 72 | + n_rng = 3 |
| 73 | + |
| 74 | + rng_circuit_case1 = get_rng_circuit(42, n_rng, test_index=False) |
| 75 | + rng_circuit_case2 = get_rng_circuit(42, n_rng, test_index=True) |
| 76 | + rng_circuit_case3 = get_rng_circuit(24, n_rng, test_index=False) |
| 77 | + |
| 78 | + # Pytket "rngX" registers to extract the RNG numbers from the results. |
| 79 | + rng_reg_names = [] |
| 80 | + for rng in range(n_rng): |
| 81 | + rng_reg_names.append(f"rng{rng}") |
| 82 | + |
| 83 | + with create_circuit_in_project( |
| 84 | + rng_circuit_case1, |
| 85 | + local_project_name, |
| 86 | + f"RNG circuit 1 for {test_case_name}", |
| 87 | + ) as rng_circ_case1_ref: |
| 88 | + with create_circuit_in_project( |
| 89 | + rng_circuit_case2, |
| 90 | + local_project_name, |
| 91 | + f"RNG circuit 2 for {test_case_name}", |
| 92 | + ) as rng_circ_case2_ref: |
| 93 | + with create_circuit_in_project( |
| 94 | + rng_circuit_case3, |
| 95 | + local_project_name, |
| 96 | + f"RNG circuit 3 for {test_case_name}", |
| 97 | + ) as rng_circ_case3_ref: |
| 98 | + my_proj = qnx.projects.get(name=local_project_name) |
| 99 | + |
| 100 | + execute_job_case1 = qnx.start_execute_job( |
| 101 | + programs=[rng_circ_case1_ref, rng_circ_case1_ref], |
| 102 | + name=f"Same seed no index RNG job for {test_case_name}", |
| 103 | + project=my_proj, |
| 104 | + backend_config=backend_config, |
| 105 | + n_shots=[n_shots, n_shots], |
| 106 | + ) |
| 107 | + execute_job_case2 = qnx.start_execute_job( |
| 108 | + programs=[rng_circ_case2_ref], |
| 109 | + name=f"Same seed with index RNG job for {test_case_name}", |
| 110 | + project=my_proj, |
| 111 | + backend_config=backend_config, |
| 112 | + n_shots=[n_shots], |
| 113 | + ) |
| 114 | + execute_job_case3 = qnx.start_execute_job( |
| 115 | + programs=[rng_circ_case3_ref], |
| 116 | + name=f"Different seed no index RNG job for {test_case_name}", |
| 117 | + project=my_proj, |
| 118 | + backend_config=backend_config, |
| 119 | + n_shots=[n_shots], |
| 120 | + ) |
| 121 | + |
| 122 | + # Case 1: Executing a circuit with the same seed and no index |
| 123 | + # multiple times should give the same RNG numbers in all shots. |
| 124 | + qnx.jobs.wait_for(execute_job_case1) |
| 125 | + results_same = qnx.jobs.results(execute_job_case1) |
| 126 | + rng1_A_result_ref = results_same[0] |
| 127 | + rng1_B_result_ref = results_same[1] |
| 128 | + assert isinstance(rng1_A_result_ref, ExecutionResultRef) |
| 129 | + assert isinstance(rng1_B_result_ref, ExecutionResultRef) |
| 130 | + |
| 131 | + rng1_A_result = rng1_A_result_ref.download_result() |
| 132 | + rng1_B_result = rng1_B_result_ref.download_result() |
| 133 | + assert isinstance(rng1_A_result, BackendResult) |
| 134 | + assert isinstance(rng1_B_result, BackendResult) |
| 135 | + |
| 136 | + for rng_reg_name in rng_reg_names: |
| 137 | + reg = [Bit(f"{rng_reg_name}", i) for i in range(32)] |
| 138 | + |
| 139 | + rng1_A_numbers = ints_from_pytket_shots( |
| 140 | + rng1_A_result.get_shots(cbits=reg) |
| 141 | + ) |
| 142 | + rng1_B_numbers = ints_from_pytket_shots( |
| 143 | + rng1_B_result.get_shots(cbits=reg) |
| 144 | + ) |
| 145 | + |
| 146 | + assert len(rng1_A_numbers) == len(rng1_B_numbers) |
| 147 | + assert rng1_A_numbers == rng1_B_numbers, ( |
| 148 | + f"RNG numbers of {rng_reg_name} generated with the same seed should be equal." |
| 149 | + ) |
| 150 | + |
| 151 | + # Case 2: Executing the same circuit with the same seed and changing the |
| 152 | + # index should give different RNG numbers in all shots. |
| 153 | + qnx.jobs.wait_for(execute_job_case2) |
| 154 | + results_index = qnx.jobs.results(execute_job_case2) |
| 155 | + rng2_result_ref = results_index[0] |
| 156 | + assert isinstance(rng2_result_ref, ExecutionResultRef) |
| 157 | + |
| 158 | + rng2_result = rng2_result_ref.download_result() |
| 159 | + assert isinstance(rng2_result, BackendResult) |
| 160 | + |
| 161 | + all_shots_numbers = [] |
| 162 | + for rng_reg_name in rng_reg_names: |
| 163 | + reg = [Bit(f"{rng_reg_name}", i) for i in range(32)] |
| 164 | + |
| 165 | + all_shots_numbers.extend( |
| 166 | + ints_from_pytket_shots(rng2_result.get_shots(cbits=reg)) |
| 167 | + ) |
| 168 | + |
| 169 | + assert len(set(all_shots_numbers)) == len(all_shots_numbers), ( |
| 170 | + "All RNG numbers should be different across shots." |
| 171 | + ) |
| 172 | + |
| 173 | + # Case 3: Executing the circuit with a different seed should give |
| 174 | + # a different RNG number than the case 1 execution. |
| 175 | + qnx.jobs.wait_for(execute_job_case3) |
| 176 | + results_diff = qnx.jobs.results(execute_job_case3) |
| 177 | + rng3_result_ref = results_diff[0] |
| 178 | + assert isinstance(rng3_result_ref, ExecutionResultRef) |
| 179 | + |
| 180 | + rng3_result = rng3_result_ref.download_result() |
| 181 | + assert isinstance(rng3_result, BackendResult) |
| 182 | + |
| 183 | + for rng_reg_name in rng_reg_names: |
| 184 | + reg = [Bit(f"{rng_reg_name}", i) for i in range(32)] |
| 185 | + |
| 186 | + rng3_numbers = ints_from_pytket_shots( |
| 187 | + rng3_result.get_shots(cbits=reg) |
| 188 | + ) |
| 189 | + |
| 190 | + assert rng1_A_numbers != rng3_numbers, ( |
| 191 | + f"RNG numbers of {rng_reg_name} generated with a different seed should be different." |
| 192 | + ) |
0 commit comments