def qpu_compiler_client( self) -> Optional[Union[Client, "HTTPCompilerClient"]]: if not self._qpu_compiler_client: endpoint: Optional[str] = None if self.qpu_compiler_endpoint: endpoint = self.qpu_compiler_endpoint elif self.session: endpoint = self.session.config.qpu_compiler_url if endpoint is not None: if endpoint.startswith(("http://", "https://")): assert isinstance(self._device, Device) and self.session is not None device_endpoint = urljoin( endpoint, f'devices/{self._device._raw["device_name"]}/') self._qpu_compiler_client = HTTPCompilerClient( endpoint=device_endpoint, session=self.session) elif endpoint.startswith("tcp://"): self._qpu_compiler_client = Client(endpoint, timeout=self.timeout) else: raise UserMessageError( "Invalid endpoint provided to QPUCompiler. Expected protocol in [http://, " f"https://, tcp://], but received endpoint {endpoint}") assert (isinstance(self._qpu_compiler_client, (Client, HTTPCompilerClient)) or self._qpu_compiler_client is None) return self._qpu_compiler_client
def native_quil_to_executable( self, nq_program: Program) -> Optional[BinaryExecutableResponse]: if not self.qpu_compiler_client: raise UserMessageError( "It looks like you're trying to compile to an executable, but " "do not have access to the QPU compiler endpoint. Make sure you " "are engaged to the QPU before trying to do this.") self._connect_qpu_compiler() if nq_program.native_quil_metadata is None: warnings.warn( "It looks like you're trying to call `native_quil_to_binary` on a " "Program that hasn't been compiled via `quil_to_native_quil`. This is " "ok if you've hand-compiled your program to our native gateset, " "but be careful!") arithmetic_response = rewrite_arithmetic(nq_program) request = BinaryExecutableRequest(quil=arithmetic_response.quil, num_shots=nq_program.num_shots) response: BinaryExecutableResponse = cast( BinaryExecutableResponse, self.qpu_compiler_client.call("native_quil_to_binary", request), ) # hack! we're storing a little extra info in the executable binary that we don't want to # expose to anyone outside of our own private lives: not the user, not the Forest server, # not anyone. response.recalculation_table = arithmetic_response.recalculation_table # type: ignore response.memory_descriptors = _collect_memory_descriptors(nq_program) response.ro_sources = _collect_classical_memory_write_locations( nq_program) return response
def get_calibration_program(self) -> Program: """ Get the Quil-T calibration program associated with the underlying QPU. A calibration program contains a number of DEFCAL, DEFWAVEFORM, and DEFFRAME instructions. In sum, those instructions describe how a Quil-T program should be translated into analog instructions for execution on hardware. :returns: A Program object containing the calibration definitions.""" self._connect_qpu_compiler() request = QuiltCalibrationsRequest(target_device=self.target_device) if not self.qpu_compiler_client: raise UserMessageError( # TODO Improve this error message with a reference to # the pyquil config docs? "It looks like you're trying to request Quil-T calibrations, but " "do not have access to the QPU compiler endpoint. Make sure you " "are engaged to the QPU or have configured qpu_compiler_endpoint " "in your pyquil configuration.") response = cast( QuiltCalibrationsResponse, self.qpu_compiler_client.call("get_quilt_calibrations", request), ) calibration_program = parse_program(response.quilt) return calibration_program
def assert_valid_auth_credential(self) -> None: """ assert_valid_auth_credential will check to make sure the user has a valid auth credential configured. This assertion is made lazily - it is called only after the user has received a 401 or 403 from forest server. See _base_connection.py::ForestSession#_refresh_auth_token. """ if self.user_auth_token is None and self.qmi_auth_token is None: raise UserMessageError( f"Your configuration does not have valid authentication credentials." f"Please visit {self.qcs_url}/auth/token to download credentials" f"and save to {self.user_auth_token_path}.")
def call(self, method: str, payload: Optional[Message] = None, *, rpc_timeout: int = 30) -> Message: """ A partially-compatible implementation of rpcq.Client#call, which allows calling rpcq methods over HTTP following the scheme: POST <endpoint>/<method> body: json-serialized <payload> This implementation is meant for use only with the QPUCompiler and is not intended to be a fully-compatible port of rpcq from ZeroMQ to HTTP. If the request succeeds (per HTTP response code), the body of the response is parsed into an RPCQ Message. If the request fails, the response body should be a JSON object with a ``message`` field indicating the cause of the failure. If present, that message is delivered to the user. :param payload: The rpcq message body. :param rpc_timeout: The number of seconds to wait to read data back from the service after connection. @see https://requests.readthedocs.io/en/master/user/advanced/#timeouts """ url = urljoin(self.endpoint, method) if payload: body = to_json(payload) # type: ignore else: body = None response = self.session.post(url, json=body, timeout=(1, rpc_timeout)) try: response.raise_for_status() except RequestException as e: message = f"QPU Compiler {method} failed: " try: contents = response.json() except Exception: contents = None if contents and contents.get("message"): message += contents.get("message") else: message += "please try again or contact support." raise UserMessageError(message) from e return cast(Message, from_json(response.text)) # type: ignore
def qpu_compiler_client(self) -> Client: if not self._qpu_compiler_client: endpoint = self.qpu_compiler_endpoint or self.session.config.qpu_compiler_url if endpoint is not None: if endpoint.startswith(("http://", "https://")): device_endpoint = urljoin( endpoint, f'devices/{self._device._raw["device_name"]}/') self._qpu_compiler_client = HTTPCompilerClient( endpoint=device_endpoint, session=self.session) elif endpoint.startswith("tcp://"): self._qpu_compiler_client = Client(endpoint, timeout=self.timeout) else: raise UserMessageError( "Invalid endpoint provided to QPUCompiler. Expected protocol in [http://, " f"https://, tcp://], but received endpoint {endpoint}") return self._qpu_compiler_client
def native_quil_to_executable( self, nq_program: Program, *, debug: bool = False) -> Optional[QuiltBinaryExecutableResponse]: if not self.qpu_compiler_client: raise UserMessageError( "It looks like you're trying to compile to an executable, but " "do not have access to the QPU compiler endpoint. Make sure you " "are engaged to the QPU before trying to do this.") self._connect_qpu_compiler() arithmetic_response = rewrite_arithmetic(nq_program) request = QuiltBinaryExecutableRequest(quilt=arithmetic_response.quil, num_shots=nq_program.num_shots) response = cast( QuiltBinaryExecutableResponse, self.qpu_compiler_client.call("native_quilt_to_binary", request, rpc_timeout=self.timeout), ) response.recalculation_table = arithmetic_response.recalculation_table # type: ignore response.memory_descriptors = _collect_memory_descriptors(nq_program) # Convert strings to MemoryReference for downstream processing. response.ro_sources = [(parse_mref(mref), source) for mref, source in response.ro_sources] # TODO (kalzoo): this is a temporary workaround to migrate memory location parsing from # the client side (where it was pre-quilt) to the service side. In some cases, the service # won't return ro_sources, and so we can fall back to parsing the change on the client side. if response.ro_sources == []: response.ro_sources = _collect_classical_memory_write_locations( nq_program) if not debug: response.debug = {} return response
def _build_client(self) -> Client: endpoint: Optional[str] = None if self.endpoint: endpoint = self.endpoint elif self.session and self.session.config.qpu_url: endpoint = self.session.config.qpu_url if endpoint is None: raise UserMessageError( """It looks like you've tried to run a program against a QPU but do not currently have a reservation on one. To reserve time on Rigetti QPUs, use the command line interface, qcs, which comes pre-installed in your QMI. From within your QMI, type: qcs reserve --lattice <lattice-name> For more information, please see the docs at https://www.rigetti.com/qcs/docs/reservations or reach out to Rigetti support at [email protected].""") logger.debug("QPU Client connecting to %s", endpoint) return Client(endpoint, auth_config=self._get_client_auth_config())
def _engage(self) -> Optional["Engagement"]: """ The heart of the QPU authorization process, ``engage`` makes a request to the dispatch server for the information needed to communicate with the QPU. This is a standard GraphQL request, authenticated using the access token retrieved from Forest Server. The response includes the endpoints to the QPU and QPU Compiler Server, along with the set of keys necessary to connect to the QPU and the time at which that key set expires. """ query = """ mutation Engage($name: String!) { engage(input: { lattice: { name: $name }}) { success message engagement { type qpu { endpoint credentials { clientPublic clientSecret serverPublic } } compiler { endpoint } expiresAt } } } """ if not self.lattice_name: logger.debug( "ForestSession requires lattice_name in order to engage") return None logger.debug("Requesting engagement from %s", self.config.dispatch_url) variables = dict(name=self.lattice_name) query_response = self._request_graphql_retry(self.config.dispatch_url, query=query, variables=variables) if query_response.get("errors"): errors = query_response.get("errors", []) error_messages = map(lambda error: error["message"], errors) # type: ignore raise UserMessageError( f"Failed to engage: {','.join(error_messages)}") engagement_response = query_response.get("data", {}).get("engage", None) if engagement_response and engagement_response.get("success") is True: logger.debug("Engagement successful") engagement_data = engagement_response.get("engagement", {}) return Engagement( client_secret_key=engagement_data.get("qpu", {}).get( "credentials", {}).get("clientSecret", "").encode("utf-8"), client_public_key=engagement_data.get("qpu", {}).get( "credentials", {}).get("clientPublic", "").encode("utf-8"), server_public_key=engagement_data.get("qpu", {}).get( "credentials", {}).get("serverPublic", "").encode("utf-8"), expires_at=engagement_data.get("expiresAt", {}), qpu_endpoint=engagement_data.get("qpu", {}).get("endpoint"), qpu_compiler_endpoint=engagement_data.get("compiler", {}).get("endpoint"), ) else: raise UserMessageError( f"Unable to engage {self.lattice_name}: " f"{engagement_response.get('message', 'No message')}")