Repository URL to install this package:
|
Version:
0.11.2 ▾
|
qiskit-ibm-runtime
/
runtime_program.py
|
|---|
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Qiskit runtime program."""
import logging
import re
from typing import Optional, Dict
from types import SimpleNamespace
from qiskit_ibm_runtime.exceptions import IBMInputValueError, IBMNotAuthorizedError
from .exceptions import IBMRuntimeError, RuntimeProgramNotFound
from .api.clients.runtime import RuntimeClient
from .api.exceptions import RequestsApiError
logger = logging.getLogger(__name__)
class RuntimeProgram:
"""Class representing program metadata.
This class contains the metadata describing a program, such as its
name, ID, description, etc.
You can use the :class:`~qiskit_ibm_runtime.QiskitRuntimeService`
to retrieve the metadata of a specific program or all programs. For example::
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
# To retrieve metadata of all programs.
programs = service.programs()
# To retrieve metadata of a single program.
program = service.program(program_id='sampler')
print(f"Program {program.name} takes parameters {program.parameters().metadata}")
"""
def __init__(
self,
program_name: str,
program_id: str,
description: str,
parameters: Optional[Dict] = None,
return_values: Optional[Dict] = None,
interim_results: Optional[Dict] = None,
max_execution_time: int = 0,
backend_requirements: Optional[Dict] = None,
creation_date: str = "",
update_date: str = "",
is_public: Optional[bool] = False,
data: str = "",
api_client: Optional[RuntimeClient] = None,
) -> None:
"""RuntimeProgram constructor.
Args:
program_name: Program name.
program_id: Program ID.
description: Program description.
parameters: Documentation on program parameters.
return_values: Documentation on program return values.
interim_results: Documentation on program interim results.
max_execution_time: Maximum execution time.
backend_requirements: Backend requirements.
creation_date: Program creation date.
update_date: Program last updated date.
is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you.
data: Program data.
api_client: Runtime api client.
"""
self._name = program_name
self._id = program_id
self._description = description
self._max_execution_time = max_execution_time
self._backend_requirements = backend_requirements or {}
self._parameters = parameters or {}
self._return_values = return_values or {}
self._interim_results = interim_results or {}
self._creation_date = creation_date
self._update_date = update_date
self._is_public = is_public
self._data = data
self._api_client = api_client
def __str__(self) -> str:
def _format_common(schema: Dict) -> None:
"""Add title, description and property details to `formatted`."""
if "description" in schema:
formatted.append(" " * 4 + "Description: {}".format(schema["description"]))
if "type" in schema:
formatted.append(" " * 4 + "Type: {}".format(str(schema["type"])))
if "properties" in schema:
formatted.append(" " * 4 + "Properties:")
for property_name, property_value in schema["properties"].items():
formatted.append(" " * 8 + "- " + property_name + ":")
for key, value in property_value.items():
formatted.append(
" " * 12 + "{}: {}".format(camel_to_sentence_case(key), str(value))
)
formatted.append(
" " * 12 + "Required: " + str(property_name in schema.get("required", []))
)
def _format_backend_requirements(schema: Dict) -> None:
"""Add backend requirements details to `formatted`."""
if "min_num_qubits" in schema:
formatted.append(
" " * 4 + "Minimum number of qubits: {}".format(str(schema["min_num_qubits"]))
)
for key, value in schema.items():
if key not in ["min_num_qubits"]:
formatted.append(
" " * 4 + "{}: {}".format(snake_to_sentence_case(key), str(value))
)
def snake_to_sentence_case(snake_case_text: str) -> str:
"""Converts snake_case to Sentence case"""
snake_case_words = snake_case_text.split("_")
return camel_to_sentence_case(
snake_case_words[0] + "".join(x.title() for x in snake_case_words[1:])
)
def camel_to_sentence_case(camel_case_text: str) -> str:
"""Converts camelCase to Sentence case"""
if camel_case_text == "":
return camel_case_text
sentence_case_text = re.sub("([A-Z])", r" \1", camel_case_text)
return sentence_case_text[:1].upper() + sentence_case_text[1:].lower()
formatted = [
f"{self.program_id}:",
f" Name: {self.name}",
f" Description: {self.description}",
f" Creation date: {self.creation_date}",
f" Update date: {self.update_date}",
f" Max execution time: {self.max_execution_time}",
]
formatted.append(" Backend requirements:")
if self._backend_requirements:
_format_backend_requirements(self._backend_requirements)
else:
formatted.append(" " * 4 + "none")
formatted.append(" Input parameters:")
if self._parameters:
_format_common(self._parameters)
else:
formatted.append(" " * 4 + "none")
formatted.append(" Interim results:")
if self._interim_results:
_format_common(self._interim_results)
else:
formatted.append(" " * 4 + "none")
formatted.append(" Returns:")
if self._return_values:
_format_common(self._return_values)
else:
formatted.append(" " * 4 + "none")
return "\n".join(formatted)
def to_dict(self) -> Dict:
"""Convert program metadata to dictionary format.
Returns:
Program metadata in dictionary format.
"""
return {
"program_id": self.program_id,
"name": self.name,
"description": self.description,
"max_execution_time": self.max_execution_time,
"backend_requirements": self.backend_requirements,
"parameters": self.parameters(),
"return_values": self.return_values,
"interim_results": self.interim_results,
"is_public": self._is_public,
}
def parameters(self) -> "ParameterNamespace":
"""Program parameter namespace.
You can use the returned namespace to assign parameter values and pass
the namespace to :meth:`qiskit_ibm_runtime.QiskitRuntimeService.run`.
The namespace allows you to use auto-completion to find program parameters.
Note that each call to this method returns a new namespace instance and
does not include any modification to the previous instance.
Returns:
Program parameter namespace.
"""
return ParameterNamespace(self._parameters)
@property
def program_id(self) -> str:
"""Program ID.
Returns:
Program ID.
"""
return self._id
@property
def name(self) -> str:
"""Program name.
Returns:
Program name.
"""
return self._name
@property
def description(self) -> str:
"""Program description.
Returns:
Program description.
"""
return self._description
@property
def return_values(self) -> Dict:
"""Program return value definitions.
Returns:
Return value definitions for this program.
"""
return self._return_values
@property
def interim_results(self) -> Dict:
"""Program interim result definitions.
Returns:
Interim result definitions for this program.
"""
return self._interim_results
@property
def max_execution_time(self) -> int:
"""Maximum execution time in seconds.
A program execution exceeding this time will be forcibly terminated.
Returns:
Maximum execution time.
"""
return self._max_execution_time
@property
def backend_requirements(self) -> Dict:
"""Backend requirements.
Returns:
Backend requirements for this program.
"""
return self._backend_requirements
@property
def creation_date(self) -> str:
"""Program creation date.
Returns:
Program creation date.
"""
return self._creation_date
@property
def update_date(self) -> str:
"""Program last updated date.
Returns:
Program last updated date.
"""
return self._update_date
@property
def is_public(self) -> bool:
"""Whether the program is visible to all.
Returns:
Whether the program is public.
"""
return self._is_public
@property
def data(self) -> str:
"""Program data.
Returns:
Program data.
Raises:
IBMNotAuthorizedError: if user is not the program author.
"""
if not self._data:
self._refresh()
if not self._data:
raise IBMNotAuthorizedError(
"Only program authors are authorized to retrieve program data"
)
return self._data
def _refresh(self) -> None:
"""Refresh program data and metadata
Raises:
RuntimeProgramNotFound: If the program does not exist.
IBMRuntimeError: If the request failed.
"""
try:
response = self._api_client.program_get(self._id)
except RequestsApiError as ex:
if ex.status_code == 404:
raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None
raise IBMRuntimeError(f"Failed to get program: {ex}") from None
self._backend_requirements = {}
self._parameters = {}
self._return_values = {}
self._interim_results = {}
if "spec" in response:
self._backend_requirements = response["spec"].get("backend_requirements", {})
self._parameters = response["spec"].get("parameters", {})
self._return_values = response["spec"].get("return_values", {})
self._interim_results = response["spec"].get("interim_results", {})
self._name = response["name"]
self._id = response["id"]
self._description = response.get("description", "")
self._max_execution_time = response.get("cost", 0)
self._creation_date = response.get("creation_date", "")
self._update_date = response.get("update_date", "")
self._is_public = response.get("is_public", False)
self._data = response.get("data", "")
def __repr__(self) -> str:
return f"<{self.__class__.__name__}('{self._id}')>"
class ParameterNamespace(SimpleNamespace):
"""A namespace for program parameters with validation.
This class provides a namespace for program parameters with auto-completion
and validation support.
"""
def __init__(self, parameters: Dict):
"""ParameterNamespace constructor.
Args:
parameters: The program's input parameters.
"""
super().__init__()
# Allow access to the raw program parameters dict
self.__metadata = parameters
# For localized logic, create store of parameters in dictionary
self.__program_params: dict = {}
for parameter_name, parameter_value in parameters.get("properties", {}).items():
# (1) Add parameters to a dict by name
setattr(self, parameter_name, None)
# (2) Store the program params for validation
self.__program_params[parameter_name] = parameter_value
@property
def metadata(self) -> Dict:
"""Returns the parameter metadata"""
return self.__metadata
def validate(self) -> None:
"""Validate program input values.
Note:
This method only verifies that required parameters have values. It
does not fail the validation if the namespace has extraneous parameters.
Raises:
IBMInputValueError: if validation fails
"""
# Iterate through the user's stored inputs
for parameter_name, _ in self.__program_params.items():
# Set invariants: User-specified parameter value (value) and if it's required (req)
value = getattr(self, parameter_name, None)
# Check there exists a program parameter of that name.
if value is None and parameter_name in self.metadata.get("required", []):
raise IBMInputValueError("Param (%s) missing required value!" % parameter_name)
def __str__(self) -> str:
"""Creates string representation of object"""
# Header
header = "| {:10.10} | {:12.12} | {:12.12} " "| {:8.8} | {:>15} |".format(
"Name", "Value", "Type", "Required", "Description"
)
params_str = "\n".join(
[
"| {:10.10} | {:12.12} | {:12.12}| {:8.8} | {:>15} |".format(
parameter_name,
str(getattr(self, parameter_name, "None")),
str(parameter_value.get("type", "None")),
str(parameter_name in self.metadata.get("required", [])),
str(parameter_value.get("description", "None")),
)
for parameter_name, parameter_value in self.__program_params.items()
]
)
return "ParameterNamespace (Values):\n%s\n%s\n%s" % (
header,
"-" * len(header),
params_str,
)
def to_dict(self) -> Dict:
"""Convert to dictionary."""
return self.__program_params