def test_run_program__returns_job_info( mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, mocker: MockerFixture, ): qpu_client = QPUClient(quantum_processor_id="some-processor", engagement_manager=mock_engagement_manager) mock_engagement_manager.get_engagement.return_value = engagement( quantum_processor_id="some-processor", seconds_left=10, credentials=engagement_credentials, port=1234, ) rpcq_client = patch_rpcq_client(mocker=mocker, return_value="some-job") request = RunProgramRequest( id="some-qpu-request", priority=1, program="encrypted-program", patch_values={"foo": [42]}, ) assert qpu_client.run_program(request) == RunProgramResponse( job_id="some-job") rpcq_client.call.assert_called_once_with( "execute_qpu_request", request=rpcq.messages.QPURequest( id="some-qpu-request", program="encrypted-program", patch_values={"foo": [42]}, ), priority=request.priority, user=None, )
def test__calculate_timeout__engagement_is_shorter_than_timeout( freezer, # This freezes time so that timeout can be compared with == mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, ): """ Tests that the original request timeout is truncated when time until engagement expiration is sooner than timeout. """ mock_engagement_manager.get_engagement.return_value = {} client_timeout = 1.0 seconds_left = 0.5 qpu_client = QPUClient( quantum_processor_id="some-processor", engagement_manager=mock_engagement_manager, request_timeout=client_timeout, ) _engagement = engagement( quantum_processor_id="some-processor", seconds_left=seconds_left, credentials=engagement_credentials, port=1234, ) mock_engagement_manager.get_engagement.return_value = _engagement assert qpu_client._calculate_timeout( engagement=_engagement) == seconds_left
def test_init__sets_processor_and_timeout( mock_engagement_manager: mock.MagicMock): qpu_client = QPUClient( quantum_processor_id="some-processor", engagement_manager=mock_engagement_manager, request_timeout=3.14, ) assert qpu_client.quantum_processor_id == "some-processor" assert qpu_client.timeout == 3.14
def test_get_buffers__returns_buffers_for_job( mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, mocker: MockerFixture, ): qpu_client = QPUClient(quantum_processor_id="some-processor", engagement_manager=mock_engagement_manager) mock_engagement_manager.get_engagement.return_value = engagement( quantum_processor_id="some-processor", seconds_left=10, credentials=engagement_credentials, port=1234, ) rpcq_client = patch_rpcq_client(mocker=mocker, return_value={ "ro": { "shape": (1000, 2), "dtype": "float64", "data": b"buffer-data", }, }) job_id = "some-job" wait = True request = GetBuffersRequest( job_id=job_id, wait=wait, ) assert qpu_client.get_buffers(request) == GetBuffersResponse( buffers={ "ro": BufferResponse( shape=(1000, 2), dtype="float64", data=b"buffer-data", ) }) rpcq_client.call.assert_called_once_with( "get_buffers", job_id=job_id, wait=wait, )
def test_fetches_engagement_for_quantum_processor_on_request( mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, mocker: MockerFixture, ): processor_id = "some-processor" qpu_client = QPUClient( quantum_processor_id=processor_id, engagement_manager=mock_engagement_manager, request_timeout=3.14, ) def mock_get_engagement( quantum_processor_id: str, request_timeout: float = 10.0, endpoint_id: Optional[str] = None) -> EngagementWithCredentials: assert quantum_processor_id == processor_id assert request_timeout == qpu_client.timeout return engagement( quantum_processor_id="some-processor", seconds_left=9999, credentials=engagement_credentials, port=1234, ) mock_engagement_manager.get_engagement.side_effect = mock_get_engagement patch_rpcq_client(mocker=mocker, return_value="") request = RunProgramRequest( id="", priority=0, program="", patch_values={}, ) qpu_client.run_program(request) mock_engagement_manager.get_engagement.assert_called_once_with( endpoint_id=None, quantum_processor_id=processor_id, request_timeout=qpu_client.timeout, )
def __init__( self, *, quantum_processor_id: str, priority: int = 1, timeout: float = 10.0, client_configuration: Optional[QCSClientConfiguration] = None, engagement_manager: Optional[EngagementManager] = None, endpoint_id: Optional[str] = None, ) -> None: """ A connection to the QPU. :param quantum_processor_id: Processor to run against. :param priority: The priority with which to insert jobs into the QPU queue. Lower integers correspond to higher priority. :param timeout: Time limit for requests, in seconds. :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. :param endpoint_id: Optional endpoint ID to be used for engagement. :param engagement_manager: Optional engagement manager. If none is provided, a default one will be created. """ super().__init__() self.priority = priority client_configuration = client_configuration or QCSClientConfiguration.load( ) engagement_manager = engagement_manager or EngagementManager( client_configuration=client_configuration) self._qpu_client = QPUClient( quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id, engagement_manager=engagement_manager, request_timeout=timeout, ) self._last_results: Dict[str, np.ndarray] = {} self._memory_results: Dict[str, Optional[np.ndarray]] = defaultdict( lambda: None)
def test__calculate_timeout__engagement_is_longer_than_timeout( mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, ): """ Tests that the original request timeout is honored when time until engagement expiration is longer than timeout. """ client_timeout = 0.1 qpu_client = QPUClient( quantum_processor_id="some-processor", engagement_manager=mock_engagement_manager, request_timeout=client_timeout, ) _engagement = engagement( quantum_processor_id="some-processor", seconds_left=qpu_client.timeout * 10, credentials=engagement_credentials, port=1234, ) mock_engagement_manager.get_engagement.return_value = _engagement assert qpu_client._calculate_timeout( engagement=_engagement) == client_timeout
def test_run_program__retries_on_timeout( mock_engagement_manager: mock.MagicMock, engagement_credentials: EngagementCredentials, mocker: MockerFixture, ): """Test that if a run request times out, it will be retried. A real-world example is a request crossing the boundary between two contiguous engagements. """ # SETUP processor_id = "some-processor" job_id = "some-job" qpu_client = QPUClient( quantum_processor_id=processor_id, engagement_manager=mock_engagement_manager, request_timeout=1.0, ) mock_engagement_manager.get_engagement.return_value = engagement( quantum_processor_id=processor_id, seconds_left=0, credentials=engagement_credentials, port=1234, ) rpcq_client = patch_rpcq_client(mocker=mocker, return_value=None) rpcq_client.call.side_effect = [ TimeoutError, # First request must look like it timed out so we can verify retry job_id, ] request_kwargs = { "id": "TestingContiguous", "patch_values": {}, "program": "" } request = RunProgramRequest( # Thing we give to QPUClient priority=0, **request_kwargs, ) # ACT assert qpu_client.run_program(request) == RunProgramResponse(job_id=job_id) # ASSERT # Engagement should be fetched twice, once per RPC call mock_engagement_manager.get_engagement.assert_has_calls([ mocker.call(quantum_processor_id='some-processor', request_timeout=1.0, endpoint_id=None), mocker.call(quantum_processor_id='some-processor', request_timeout=1.0, endpoint_id=None), ]) # RPC call should happen twice since the first one times out qpu_request = QPURequest( **request_kwargs) # Thing QPUClient gives to rpcq.Client rpcq_client.call.assert_has_calls([ mocker.call("execute_qpu_request", request=qpu_request, priority=0, user=None), mocker.call("execute_qpu_request", request=qpu_request, priority=0, user=None), ])
class QPU(QAM[QPUExecuteResponse]): def __init__( self, *, quantum_processor_id: str, priority: int = 1, timeout: float = 10.0, client_configuration: Optional[QCSClientConfiguration] = None, engagement_manager: Optional[EngagementManager] = None, endpoint_id: Optional[str] = None, ) -> None: """ A connection to the QPU. :param quantum_processor_id: Processor to run against. :param priority: The priority with which to insert jobs into the QPU queue. Lower integers correspond to higher priority. :param timeout: Time limit for requests, in seconds. :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. :param endpoint_id: Optional endpoint ID to be used for engagement. :param engagement_manager: Optional engagement manager. If none is provided, a default one will be created. """ super().__init__() self.priority = priority client_configuration = client_configuration or QCSClientConfiguration.load( ) engagement_manager = engagement_manager or EngagementManager( client_configuration=client_configuration) self._qpu_client = QPUClient( quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id, engagement_manager=engagement_manager, request_timeout=timeout, ) self._last_results: Dict[str, np.ndarray] = {} self._memory_results: Dict[str, Optional[np.ndarray]] = defaultdict( lambda: None) @property def quantum_processor_id(self) -> str: """ID of quantum processor targeted.""" return self._qpu_client.quantum_processor_id def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: """ Enqueue a job for execution on the QPU. Returns a ``QPUExecuteResponse``, a job descriptor which should be passed directly to ``QPU.get_result`` to retrieve results. """ executable = executable.copy() assert isinstance( executable, EncryptedProgram ), "QPU#execute requires an rpcq.EncryptedProgram. Create one with QuantumComputer#compile" assert ( executable.ro_sources is not None ), "To run on a QPU, a program must include ``MEASURE``, ``CAPTURE``, and/or ``RAW-CAPTURE`` instructions" request = RunProgramRequest( id=str(uuid.uuid4()), priority=self.priority, program=executable.program, patch_values=self._build_patch_values(executable), ) job_id = self._qpu_client.run_program(request).job_id return QPUExecuteResponse(_executable=executable, job_id=job_id) def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: """ Retrieve results from execution on the QPU. """ raw_results = self._get_buffers(execute_response.job_id) ro_sources = execute_response._executable.ro_sources result_memory = {} if raw_results is not None: extracted = _extract_memory_regions( execute_response._executable.memory_descriptors, ro_sources, raw_results) for name, array in extracted.items(): result_memory[name] = array elif not ro_sources: result_memory["ro"] = np.zeros((0, 0), dtype=np.int64) return QAMExecutionResult(executable=execute_response._executable, readout_data=result_memory) def _get_buffers(self, job_id: str) -> Dict[str, np.ndarray]: """ Return the decoded result buffers for particular job_id. :param job_id: Unique identifier for the job in question :return: Decoded buffers or throw an error """ request = GetBuffersRequest(job_id=job_id, wait=True) buffers = self._qpu_client.get_buffers(request).buffers return {k: decode_buffer(v) for k, v in buffers.items()} @classmethod def _build_patch_values( cls, program: EncryptedProgram) -> Dict[str, List[Union[int, float]]]: """ Construct the patch values from the program to be used in execution. """ patch_values = {} # Now that we are about to run, we have to resolve any gate parameter arithmetic that was # saved in the executable's recalculation table, and add those values to the variables shim cls._update_memory_with_recalculation_table(program=program) # Initialize our patch table assert isinstance(program, EncryptedProgram) recalculation_table = program.recalculation_table memory_ref_names = list( set(mr.name for mr in recalculation_table.keys())) if memory_ref_names: assert len(memory_ref_names) == 1, ( "We expected only one declared memory region for " "the gate parameter arithmetic replacement references.") memory_reference_name = memory_ref_names[0] patch_values[memory_reference_name] = [0.0 ] * len(recalculation_table) for name, spec in program.memory_descriptors.items(): # NOTE: right now we fake reading out measurement values into classical memory # hence we omit them here from the patch table. if any(name == mref.name for mref in program.ro_sources): continue initial_value = 0.0 if spec.type == "REAL" else 0 patch_values[name] = [initial_value] * spec.length # Fill in our patch table for k, v in program._memory.values.items(): patch_values[k.name][k.index] = v return patch_values @classmethod def _update_memory_with_recalculation_table( cls, program: EncryptedProgram) -> None: """ Update the program's memory with the final values to be patched into the gate parameters, according to the arithmetic expressions in the original program. For example:: DECLARE theta REAL DECLARE beta REAL RZ(3 * theta) 0 RZ(beta+theta) 0 gets translated to:: DECLARE theta REAL DECLARE __P REAL[2] RZ(__P[0]) 0 RZ(__P[1]) 0 and the recalculation table will contain:: { ParameterAref('__P', 0): Mul(3.0, <MemoryReference theta[0]>), ParameterAref('__P', 1): Add(<MemoryReference beta[0]>, <MemoryReference theta[0]>) } Let's say we've made the following two function calls: .. code-block:: python compiled_program.write_memory(region_name='theta', value=0.5) compiled_program.write_memory(region_name='beta', value=0.1) After executing this function, our self.variables_shim in the above example would contain the following: .. code-block:: python { ParameterAref('theta', 0): 0.5, ParameterAref('beta', 0): 0.1, ParameterAref('__P', 0): 1.5, # (3.0) * theta[0] ParameterAref('__P', 1): 0.6 # beta[0] + theta[0] } Once the _variables_shim is filled, execution continues as with regular binary patching. """ assert isinstance(program, EncryptedProgram) for mref, expression in program.recalculation_table.items(): # Replace the user-declared memory references with any values the user has written, # coerced to a float because that is how we declared it. program._memory.values[mref] = float( cls._resolve_memory_references(expression, memory=program._memory)) @classmethod def _resolve_memory_references(cls, expression: ExpressionDesignator, memory: Memory) -> Union[float, int]: """ Traverse the given Expression, and replace any Memory References with whatever values have been so far provided by the user for those memory spaces. Declared memory defaults to zero. :param expression: an Expression """ if isinstance(expression, BinaryExp): left = cls._resolve_memory_references(expression.op1, memory=memory) right = cls._resolve_memory_references(expression.op2, memory=memory) return cast(Union[float, int], expression.fn(left, right)) elif isinstance(expression, Function): return cast( Union[float, int], expression.fn( cls._resolve_memory_references(expression.expression, memory=memory)), ) elif isinstance(expression, Parameter): raise ValueError( f"Unexpected Parameter in gate expression: {expression}") elif isinstance(expression, (float, int)): return expression elif isinstance(expression, MemoryReference): return memory.values.get( ParameterAref(name=expression.name, index=expression.offset), 0) else: raise ValueError( f"Unexpected expression in gate parameter: {expression}")