Example #1
0
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,
    )
Example #2
0
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
Example #3
0
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
Example #4
0
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,
    )
Example #5
0
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,
    )
Example #6
0
    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)
Example #7
0
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
Example #8
0
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),
    ])
Example #9
0
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}")