diff --git a/qiskit/providers/ibmq/api/clients/runtime.py b/qiskit/providers/ibmq/api/clients/runtime.py index 90c5346ef..452d86773 100644 --- a/qiskit/providers/ibmq/api/clients/runtime.py +++ b/qiskit/providers/ibmq/api/clients/runtime.py @@ -13,7 +13,7 @@ """Client for accessing IBM Quantum runtime service.""" import logging -from typing import List, Dict, Union, Optional +from typing import Any, List, Dict, Union, Optional from qiskit.providers.ibmq.credentials import Credentials from qiskit.providers.ibmq.api.session import RetrySession @@ -39,7 +39,7 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: @@ -49,30 +49,22 @@ def list_programs(self) -> List[Dict]: def program_create( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Create a new program. Args: name: Name of the program. - program_data: Program data. + program_data: Program data (base64 encoded). description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: Server response. @@ -81,9 +73,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, version=version, backend_requirements=backend_requirements, - parameters=parameters, return_values=return_values, - interim_results=interim_results + is_public=is_public, spec=spec ) def program_get(self, program_id: str) -> Dict: @@ -127,7 +117,7 @@ def program_run( program_id: str, credentials: Credentials, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Run the specified program. @@ -160,7 +150,7 @@ def program_update(self, program_id: str, program_data: str) -> None: Args: program_id: Program ID. - program_data: Program data. + program_data: Program data (base64 encoded). """ self.api.program(program_id).update(program_data) diff --git a/qiskit/providers/ibmq/api/rest/runtime.py b/qiskit/providers/ibmq/api/rest/runtime.py index 81f250b74..382d9c5af 100644 --- a/qiskit/providers/ibmq/api/rest/runtime.py +++ b/qiskit/providers/ibmq/api/rest/runtime.py @@ -19,6 +19,7 @@ from .base import RestAdapterBase from ..session import RetrySession +from ...runtime.utils import RuntimeEncoder logger = logging.getLogger(__name__) @@ -54,7 +55,7 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: @@ -65,54 +66,36 @@ def list_programs(self) -> List[Dict]: def create_program( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Upload a new program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). name: Name of the program. description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: JSON response. """ url = self.get_url('programs') - data = {'name': name, - 'cost': str(max_execution_time), - 'description': description.encode(), - 'max_execution_time': max_execution_time, - 'isPublic': is_public} - if version is not None: - data['version'] = version - if backend_requirements: - data['backendRequirements'] = json.dumps(backend_requirements) - if parameters: - data['parameters'] = json.dumps({"doc": parameters}) - if return_values: - data['returnValues'] = json.dumps(return_values) - if interim_results: - data['interimResults'] = json.dumps(interim_results) - - files = {'program': (name, program_data)} # type: ignore[dict-item] - response = self.session.post(url, data=data, files=files).json() - return response + payload = {'name': name, + 'data': program_data, + 'cost': max_execution_time, + 'description': description, + 'is_public': is_public} + if spec is not None: + payload['spec'] = spec + data = json.dumps(payload) + return self.session.post(url, data=data).json() def program_run( self, @@ -121,7 +104,7 @@ def program_run( group: str, project: str, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Execute the program. @@ -140,15 +123,15 @@ def program_run( """ url = self.get_url('jobs') payload = { - 'programId': program_id, + 'program_id': program_id, 'hub': hub, 'group': group, 'project': project, 'backend': backend_name, - 'params': [params], + 'params': params, 'runtime': image } - data = json.dumps(payload) + data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> Dict: @@ -243,11 +226,11 @@ def update(self, program_data: str) -> None: """Update a program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). """ url = self.get_url("data") self.session.put(url, data=program_data, - headers={'Content-Type': 'text/plain'}) + headers={'Content-Type': 'application/octet-stream'}) class ProgramJob(RestAdapterBase): diff --git a/qiskit/providers/ibmq/runtime/__init__.py b/qiskit/providers/ibmq/runtime/__init__.py index e6fdf74d8..9c789088f 100644 --- a/qiskit/providers/ibmq/runtime/__init__.py +++ b/qiskit/providers/ibmq/runtime/__init__.py @@ -193,14 +193,11 @@ def interim_result_callback(job_id, interim_result): provider = IBMQ.load_account() program_id = provider.runtime.upload_program( data="my_vqe.py", - metadata="my_vqe_metadata.json", - version="1.2" + metadata="my_vqe_metadata.json" ) In the example above, the file ``my_vqe.py`` contains the program data, and -``my_vqe_metadata.json`` contains the program metadata. An additional -parameter ``version`` is also specified, which takes precedence over any -``version`` value specified in ``my_vqe_metadata.json``. +``my_vqe_metadata.json`` contains the program metadata. Method :meth:`IBMRuntimeService.delete_program` allows you to delete a program. diff --git a/qiskit/providers/ibmq/runtime/ibm_runtime_service.py b/qiskit/providers/ibmq/runtime/ibm_runtime_service.py index b73989190..b6d0a4531 100644 --- a/qiskit/providers/ibmq/runtime/ibm_runtime_service.py +++ b/qiskit/providers/ibmq/runtime/ibm_runtime_service.py @@ -15,15 +15,14 @@ import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json -import copy import re from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.ibmq import accountprovider # pylint: disable=unused-import from .runtime_job import RuntimeJob -from .runtime_program import RuntimeProgram, ProgramParameter, ProgramResult, ParameterNamespace -from .utils import RuntimeEncoder, RuntimeDecoder +from .runtime_program import RuntimeProgram, ParameterNamespace +from .utils import RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) from .program.result_decoder import ResultDecoder @@ -121,8 +120,9 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None if detailed: print(str(prog)) else: - print(f"Name: {prog.name}") - print(f"Description: {prog.description}") + print(f"{prog.program_id}:",) + print(f" Name: {prog.name}") + print(f" Description: {prog.description}") def programs(self, refresh: bool = False) -> List[RuntimeProgram]: """Return available runtime programs. @@ -139,7 +139,7 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: if not self._programs or refresh: self._programs = {} response = self._api_client.list_programs() - for prog_dict in response: + for prog_dict in response.get("programs", []): program = self._to_program(prog_dict) self._programs[program.program_id] = program return list(self._programs.values()) @@ -182,22 +182,27 @@ def _to_program(self, response: Dict) -> RuntimeProgram: Returns: A ``RuntimeProgram`` instance. """ - backend_req = json.loads(response.get('backendRequirements', '{}')) - params = json.loads(response.get('parameters', '{}')).get("doc", []) - ret_vals = json.loads(response.get('returnValues', '{}')) - interim_results = json.loads(response.get('interimResults', '{}')) + backend_requirements = {} + parameters = {} + return_values = {} + interim_results = {} + if "spec" in response: + backend_requirements = response["spec"].get('backend_requirements', {}) + parameters = response["spec"].get('parameters', {}) + return_values = response["spec"].get('return_values', {}) + interim_results = response["spec"].get('interim_results', {}) return RuntimeProgram(program_name=response['name'], program_id=response['id'], description=response.get('description', ""), - parameters=params, - return_values=ret_vals, + parameters=parameters, + return_values=return_values, interim_results=interim_results, max_execution_time=response.get('cost', 0), - creation_date=response.get('creationDate', ""), - version=response.get('version', "0"), - backend_requirements=backend_req, - is_public=response.get('isPublic', False)) + creation_date=response.get('creation_date', ""), + update_date=response.get('update_date', ""), + backend_requirements=backend_requirements, + is_public=response.get('is_public', False)) def run( self, @@ -246,12 +251,11 @@ def run( raise IBMQInputValueError('"image" needs to be in form of image_name:tag') backend_name = options['backend_name'] - params_str = json.dumps(inputs, cls=RuntimeEncoder) result_decoder = result_decoder or ResultDecoder response = self._api_client.program_run(program_id=program_id, credentials=self._provider.credentials, backend_name=backend_name, - params=params_str, + params=inputs, image=image) backend = self._provider.get_backend(backend_name) @@ -267,16 +271,7 @@ def run( def upload_program( self, data: str, - metadata: Optional[Union[Dict, str]] = None, - name: Optional[str] = None, - is_public: Optional[bool] = False, - max_execution_time: Optional[int] = None, - description: Optional[str] = None, - version: Optional[float] = None, - backend_requirements: Optional[str] = None, - parameters: Optional[List[ProgramParameter]] = None, - return_values: Optional[List[ProgramResult]] = None, - interim_results: Optional[List[ProgramResult]] = None + metadata: Optional[Union[Dict, str]] = None ) -> str: """Upload a runtime program. @@ -299,18 +294,23 @@ def upload_program( Args: data: Program data or path of the file containing program data to upload. metadata: Name of the program metadata file or metadata dictionary. - A metadata file needs to be in the JSON format. - See :file:`program/program_metadata_sample.yaml` for an example. - name: Name of the program. Required if not specified via `metadata`. - max_execution_time: Maximum execution time in seconds. Required if - not specified via `metadata`. - is_public: Whether the runtime program should be visible to the public. - description: Program description. Required if not specified via `metadata`. - version: Program version. The default is 1.0 if not specified. - backend_requirements: Backend requirements. - parameters: A list of program input parameters. - return_values: A list of program return values. - interim_results: A list of program interim results. + A metadata file needs to be in the JSON format. The ``parameters``, + ``return_values``, and ``interim_results`` should be defined as JSON Schema. + See :file:`program/program_metadata_sample.json` for an example. The + fields in metadata are explained below. + + * name: Name of the program. Required. + * max_execution_time: Maximum execution time in seconds. Required. + * description: Program description. Required. + * is_public: Whether the runtime program should be visible to the public. + The default is ``False``. + * spec: Specifications for backend characteristics and input parameters + required to run the program, interim results and final result. + + * backend_requirements: Backend requirements. + * parameters: Program input parameters in JSON schema format. + * return_values: Program return values in JSON schema format. + * interim_results: Program interim results in JSON schema format. Returns: Program ID. @@ -321,14 +321,7 @@ def upload_program( IBMQNotAuthorizedError: If you are not authorized to upload programs. QiskitRuntimeError: If the upload failed. """ - program_metadata = self._merge_metadata( - initial={}, - metadata=metadata, - name=name, max_execution_time=max_execution_time, - is_public=is_public, description=description, - version=version, backend_requirements=backend_requirements, - parameters=parameters, - return_values=return_values, interim_results=interim_results) + program_metadata = self._read_metadata(metadata=metadata) for req in ['name', 'description', 'max_execution_time']: if req not in program_metadata or not program_metadata[req]: @@ -340,7 +333,8 @@ def upload_program( data = file.read() try: - response = self._api_client.program_create(program_data=data.encode(), + program_data = to_base64_string(data) + response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: if ex.status_code == 409: @@ -352,21 +346,17 @@ def upload_program( raise QiskitRuntimeError(f"Failed to create program: {ex}") from None return response['id'] - def _merge_metadata( + def _read_metadata( self, - initial: Dict, - metadata: Optional[Union[Dict, str]] = None, - **kwargs: Any + metadata: Optional[Union[Dict, str]] = None ) -> Dict: - """Merge multiple copies of metadata. + """Read metadata. Args: - initial: The initial metadata. This may be mutated. metadata: Name of the program metadata file or metadata dictionary. - **kwargs: Additional metadata fields to overwrite. Returns: - Merged metadata. + Return metadata. """ upd_metadata: dict = {} if metadata is not None: @@ -374,33 +364,11 @@ def _merge_metadata( with open(metadata, 'r') as file: upd_metadata = json.load(file) else: - upd_metadata = copy.deepcopy(metadata) - - self._tuple_to_dict(initial) - initial.update(upd_metadata) - - self._tuple_to_dict(kwargs) - for key, val in kwargs.items(): - if val is not None: - initial[key] = val - + upd_metadata = metadata # TODO validate metadata format - metadata_keys = ['name', 'max_execution_time', 'description', 'version', - 'backend_requirements', 'parameters', 'return_values', - 'interim_results', 'is_public'] - return {key: val for key, val in initial.items() if key in metadata_keys} - - def _tuple_to_dict(self, metadata: Dict) -> None: - """Convert fields in metadata from named tuples to dictionaries. - - Args: - metadata: Metadata to be converted. - """ - for key in ['parameters', 'return_values', 'interim_results']: - doc_list = metadata.pop(key, None) - if not doc_list or isinstance(doc_list[0], dict): - continue - metadata[key] = [dict(elem._asdict()) for elem in doc_list] + metadata_keys = ['name', 'max_execution_time', 'description', + 'spec', 'is_public'] + return {key: val for key, val in upd_metadata.items() if key in metadata_keys} def update_program( self, @@ -412,12 +380,22 @@ def update_program( Args: program_id: Program ID. data: Program data or path of the file containing program data to upload. + + Raises: + RuntimeProgramNotFound: If the program doesn't exist. + QiskitRuntimeError: If the request failed. """ if "def main(" not in data: # This is the program file with open(data, "r") as file: data = file.read() - self._api_client.program_update(program_id, data) + try: + program_data = to_base64_string(data) + self._api_client.program_update(program_id, program_data) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise QiskitRuntimeError(f"Failed to update program: {ex}") from None def delete_program(self, program_id: str) -> None: """Delete a runtime program. diff --git a/qiskit/providers/ibmq/runtime/program/program_metadata_sample.json b/qiskit/providers/ibmq/runtime/program/program_metadata_sample.json index a38c18fd6..6449b474b 100644 --- a/qiskit/providers/ibmq/runtime/program/program_metadata_sample.json +++ b/qiskit/providers/ibmq/runtime/program/program_metadata_sample.json @@ -2,16 +2,38 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "version": 1.0, - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} - ], - "return_values": [ - {"name": "-", "description": "A string that says 'All done!'.", "type": "string"} - ], - "interim_results": [ - {"name": "iteration", "description": "Iteration number.", "type": "int"}, - {"name": "counts", "description": "Histogram data of the circuit result.", "type": "dict"} - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "type": "object", + "properties": { + "iterations": { + "description": "Number of iterations to run. Each iteration generates and runs a random circuit.", + "type": "integer" + } + }, + "required": [ + "iterations" + ] + }, + "return_values": { + "type": "string", + "description": "A string that says 'All done!'." + }, + "interim_results": { + "type": "object", + "properties": { + "iteration": { + "description": "Iteration number.", + "type": "integer" + }, + "counts": { + "description": "Histogram data of the circuit result.", + "type": "object" + } + } + } + } } \ No newline at end of file diff --git a/qiskit/providers/ibmq/runtime/runtime_program.py b/qiskit/providers/ibmq/runtime/runtime_program.py index ee439282b..b769886ec 100644 --- a/qiskit/providers/ibmq/runtime/runtime_program.py +++ b/qiskit/providers/ibmq/runtime/runtime_program.py @@ -13,7 +13,8 @@ """Qiskit runtime program.""" import logging -from typing import Optional, List, NamedTuple, Dict +import re +from typing import Optional, List, Dict from types import SimpleNamespace from qiskit.providers.ibmq.exceptions import IBMQInputValueError @@ -47,13 +48,13 @@ def __init__( program_name: str, program_id: str, description: str, - parameters: Optional[List] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None, + parameters: Optional[Dict] = None, + return_values: Optional[Dict] = None, + interim_results: Optional[Dict] = None, max_execution_time: int = 0, - version: str = "0", backend_requirements: Optional[Dict] = None, creation_date: str = "", + update_date: str = "", is_public: Optional[bool] = False ) -> None: """RuntimeProgram constructor. @@ -66,59 +67,54 @@ def __init__( return_values: Documentation on program return values. interim_results: Documentation on program interim results. max_execution_time: Maximum execution time. - version: Program version. 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. """ self._name = program_name self._id = program_id self._description = description self._max_execution_time = max_execution_time - self._version = version self._backend_requirements = backend_requirements or {} - self._parameters: List[ProgramParameter] = [] - self._return_values: List[ProgramResult] = [] - self._interim_results: List[ProgramResult] = [] + 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 - if parameters: - for param in parameters: - self._parameters.append( - ProgramParameter(name=param['name'], - description=param['description'], - type=param['type'], - required=param['required'])) - if return_values is not None: - for ret in return_values: - self._return_values.append(ProgramResult(name=ret['name'], - description=ret['description'], - type=ret['type'])) - if interim_results is not None: - for intret in interim_results: - self._interim_results.append(ProgramResult(name=intret['name'], - description=intret['description'], - type=intret['type'])) - def __str__(self) -> str: - def _format_common(items: List) -> None: - """Add name, description, and type to `formatted`.""" - for item in items: - formatted.append(" "*4 + "- " + item.name + ":") - formatted.append(" "*6 + "Description: " + item.description) - formatted.append(" "*6 + "Type: " + item.type) - if hasattr(item, 'required'): - formatted.append(" "*6 + "Required: " + str(item.required)) + 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(sentence_case(key), str(value))) + formatted.append(" "*12 + "Required: " + + str(property_name in schema.get("required", []))) + + def 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" Version: {self.version}", f" Creation date: {self.creation_date}", - f" Max execution time: {self.max_execution_time}", - f" Input parameters:"] + f" Update date: {self.update_date}", + f" Max execution time: {self.max_execution_time}"] + formatted.append(" Input parameters:") if self._parameters: _format_common(self._parameters) else: @@ -148,7 +144,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "max_execution_time": self.max_execution_time, - "version": self.version, "backend_requirements": self.backend_requirements, "parameters": self.parameters(), "return_values": self.return_values, @@ -199,7 +194,7 @@ def description(self) -> str: return self._description @property - def return_values(self) -> List['ProgramResult']: + def return_values(self) -> Dict: """Program return value definitions. Returns: @@ -208,7 +203,7 @@ def return_values(self) -> List['ProgramResult']: return self._return_values @property - def interim_results(self) -> List['ProgramResult']: + def interim_results(self) -> Dict: """Program interim result definitions. Returns: @@ -227,15 +222,6 @@ def max_execution_time(self) -> int: """ return self._max_execution_time - @property - def version(self) -> str: - """Program version. - - Returns: - Program version. - """ - return self._version - @property def backend_requirements(self) -> Dict: """Backend requirements. @@ -254,6 +240,15 @@ def creation_date(self) -> str: """ 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. @@ -264,21 +259,6 @@ def is_public(self) -> bool: return self._is_public -class ProgramParameter(NamedTuple): - """Program parameter.""" - name: str - description: str - type: str - required: bool - - -class ProgramResult(NamedTuple): - """Program result.""" - name: str - description: str - type: str - - class ParameterNamespace(SimpleNamespace): """ A namespace for program parameters with validation. @@ -286,26 +266,26 @@ class ParameterNamespace(SimpleNamespace): and validation support. """ - def __init__(self, params: List[ProgramParameter]): + def __init__(self, parameters: Dict): """ParameterNamespace constructor. Args: - params: The program's input parameters. + parameters: The program's input parameters. """ super().__init__() - # Allow access to the raw program parameters list - self.__metadata = params + # 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 param in params: + for parameter_name, parameter_value in parameters.get("properties", {}).items(): # (1) Add parameters to a dict by name - setattr(self, param.name, None) + setattr(self, parameter_name, None) # (2) Store the program params for validation - self.__program_params[param.name] = param + self.__program_params[parameter_name] = parameter_value @property - def metadata(self) -> List[ProgramParameter]: + def metadata(self) -> Dict: """Returns the parameter metadata""" return self.__metadata @@ -321,12 +301,12 @@ def validate(self) -> None: """ # Iterate through the user's stored inputs - for param_name, program_param in self.__program_params.items(): - # Set invariants: User-specified parameter value (value) and whether it's required (req) - value = getattr(self, param_name, None) + for parameter_name, parameter_value 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 program_param.required: - raise IBMQInputValueError('Param (%s) missing required value!' % param_name) + if value is None and parameter_name in self.metadata.get("required", []): + raise IBMQInputValueError('Param (%s) missing required value!' % parameter_name) def __str__(self) -> str: """Creates string representation of object""" @@ -339,15 +319,14 @@ def __str__(self) -> str: 'Required', 'Description' ) - # List of ProgramParameter objects (str) params_str = '\n'.join([ '| {:10.10} | {:12.12} | {:12.12}| {:8.8} | {:>15} |'.format( - param.name, - str(getattr(self, param.name, 'None')), - param.type, - str(param.required), - param.description - ) for param in self.__program_params.values()]) + 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) diff --git a/qiskit/providers/ibmq/runtime/utils.py b/qiskit/providers/ibmq/runtime/utils.py index 9290dfaee..18e8ff8b4 100644 --- a/qiskit/providers/ibmq/runtime/utils.py +++ b/qiskit/providers/ibmq/runtime/utils.py @@ -40,6 +40,18 @@ from qiskit.result import Result +def to_base64_string(data: str) -> str: + """Convert string to base64 string. + + Args: + data: string to convert + + Returns: + data as base64 string + """ + return base64.b64encode(data.encode('utf-8')).decode('utf-8') + + def _serialize_and_encode( data: Any, serializer: Callable, diff --git a/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml new file mode 100644 index 000000000..05c312841 --- /dev/null +++ b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can view the last updated date of a runtime program using + :attr:`~qiskit.providers.ibmq.runtime.RuntimeProgram.update_date` property. diff --git a/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml new file mode 100644 index 000000000..099dcd222 --- /dev/null +++ b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Runtime programs will no longer have a ``version`` field. diff --git a/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml new file mode 100644 index 000000000..dcc379f66 --- /dev/null +++ b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + :meth:`qiskit.provider.ibmq.runtime.IBMRuntimeService.upload_program` now takes only two + parameters, ``data``, which is the program passed as a string or the path to the program + file and the ``metadata``, which is passed as a dictionary or path to the metadata JSON file. + In ``metadata`` the ``backend_requirements``, ``parameters``, ``return_values`` and + ``interim_results`` are now grouped under a specifications ``spec`` section. + ``parameters``, ``return_values`` and ``interim_results`` should now be specified as + JSON Schema. diff --git a/test/ibmq/runtime/fake_runtime_client.py b/test/ibmq/runtime/fake_runtime_client.py index cc017266e..e9ccebd9b 100644 --- a/test/ibmq/runtime/fake_runtime_client.py +++ b/test/ibmq/runtime/fake_runtime_client.py @@ -26,7 +26,7 @@ class BaseFakeProgram: """Base class for faking a program.""" - def __init__(self, program_id, name, data, cost, description, version="1.0", + def __init__(self, program_id, name, data, cost, description, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Initialize a fake program.""" @@ -35,7 +35,6 @@ def __init__(self, program_id, name, data, cost, description, version="1.0", self._data = data self._cost = cost self._description = description - self._version = version self._backend_requirements = backend_requirements self._parameters = parameters self._return_values = return_values @@ -48,18 +47,20 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'version': self._version, - 'isPublic': self._is_public} + 'is_public': self._is_public, + 'creation_date': '2021-09-13T17:27:42Z', + 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data + out['spec'] = {} if self._backend_requirements: - out['backendRequirements'] = json.dumps(self._backend_requirements) + out['spec']['backend_requirements'] = self._backend_requirements if self._parameters: - out['parameters'] = json.dumps({"doc": self._parameters}) + out['spec']['parameters'] = self._parameters if self._return_values: - out['returnValues'] = json.dumps(self._return_values) + out['spec']['return_values'] = self._return_values if self._interim_results: - out['interimResults'] = json.dumps(self._interim_results) + out['spec']['interim_results'] = self._interim_results return out @@ -231,25 +232,25 @@ def set_final_status(self, final_status): self._final_status = final_status def list_programs(self): - """List all progrmas.""" + """List all programs.""" programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) - return programs + return {"programs": programs} - def program_create(self, program_data, name, description, max_execution_time, version="1.0", - backend_requirements=None, parameters=None, return_values=None, - interim_results=None, is_public=False): + def program_create(self, program_data, name, description, max_execution_time, + spec=None, is_public=False): """Create a program.""" - if isinstance(program_data, str): - with open(program_data, 'rb') as file: - program_data = file.read() program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) + backend_requirements = spec.get('backend_requirements', None) + parameters = spec.get('parameters', None) + return_values = spec.get('return_values', None) + interim_results = spec.get('interim_results', None) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, - description=description, version=version, backend_requirements=backend_requirements, + description=description, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results, is_public=is_public) return {'id': program_id} diff --git a/test/ibmq/runtime/test_runtime.py b/test/ibmq/runtime/test_runtime.py index 5d24eb5ed..7870964bb 100644 --- a/test/ibmq/runtime/test_runtime.py +++ b/test/ibmq/runtime/test_runtime.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import json import os from io import StringIO @@ -54,8 +55,7 @@ from qiskit.providers.ibmq.runtime.constants import API_TO_JOB_ERROR_MESSAGE from qiskit.providers.ibmq.runtime.exceptions import (RuntimeProgramNotFound, RuntimeJobFailureError) -from qiskit.providers.ibmq.runtime.runtime_program import ( - ParameterNamespace, ProgramParameter, ProgramResult) +from qiskit.providers.ibmq.runtime.runtime_program import ParameterNamespace from ...ibmqtestcase import IBMQTestCase from .fake_runtime_client import (BaseFakeRuntimeClient, FailedRanTooLongRuntimeJob, @@ -70,17 +70,50 @@ class TestRuntime(IBMQTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "version": "0.1", - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, - {'name': 'param2', 'description': 'Desc 2', 'type': 'int', 'required': False}], - "return_values": [ - {"name": "ret_val", "description": "Some return value.", "type": "string"} - ], - "interim_results": [ - {"name": "int_res", "description": "Some interim result", "type": "string"}, - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "properties": { + "param1": { + "description": "Desc 1", + "type": "string", + "enum": [ + "a", + "b", + "c" + ] + }, + "param2": { + "description": "Desc 2", + "type": "integer", + "min": 0 + } + }, + "required": [ + "param1" + ] + }, + "return_values": { + "type": "object", + "description": "Return values", + "properties": { + "ret_val": { + "description": "Some return value.", + "type": "string" + } + } + }, + "interim_results": { + "properties": { + "int_res": { + "description": "Some interim result", + "type": "string" + } + } + } + } } def setUp(self): @@ -286,13 +319,13 @@ def test_print_programs(self): for prog in programs: self.assertIn(prog.program_id, stdout) self.assertIn(prog.name, stdout) - self.assertNotIn(prog.version, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) self.runtime.pprint_programs(detailed=True) stdout_detailed = mock_stdout.getvalue() for prog in programs: self.assertIn(prog.program_id, stdout_detailed) self.assertIn(prog.name, stdout_detailed) - self.assertIn(prog.version, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) def test_upload_program(self): """Test uploading a program.""" @@ -600,28 +633,17 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) - self.assertEqual(self.DEFAULT_METADATA["version"], program.version) - self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) + self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], program.backend_requirements) - self.assertEqual([ProgramParameter(**param) for param in - self.DEFAULT_METADATA['parameters']], + self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], program.parameters().metadata) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['return_values']], + self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], program.return_values) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['interim_results']], + self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], program.interim_results) - def test_metadata_combined(self): - """Test combining metadata""" - update_metadata = {"version": "1.2", "max_execution_time": 600} - program_id = self.runtime.upload_program( - data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) - program = self.runtime.program(program_id) - self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - self.assertEqual(update_metadata["version"], program.version) - def test_different_providers(self): """Test retrieving job submitted with different provider.""" program_id = self._upload_program() @@ -638,12 +660,13 @@ def _upload_program(self, name=None, max_execution_time=300, """Upload a new program.""" name = name or uuid.uuid4().hex data = "def main() {}" + metadata = copy.deepcopy(self.DEFAULT_METADATA) + metadata.update(name=name) + metadata.update(is_public=is_public) + metadata.update(max_execution_time=max_execution_time) program_id = self.runtime.upload_program( - name=name, data=data, - is_public=is_public, - max_execution_time=max_execution_time, - description="A test program") + metadata=metadata) return program_id def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None, diff --git a/test/ibmq/runtime/test_runtime_integration.py b/test/ibmq/runtime/test_runtime_integration.py index 0e3965608..158cd1928 100644 --- a/test/ibmq/runtime/test_runtime_integration.py +++ b/test/ibmq/runtime/test_runtime_integration.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import unittest import os import uuid @@ -84,11 +85,12 @@ def setUpClass(cls, backend): cls.backend = backend cls.poll_time = 1 if backend.configuration().simulator else 5 cls.provider = backend.provider() + metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) + metadata['name'] = cls._get_program_name() try: cls.program_id = cls.provider.runtime.upload_program( - name=cls._get_program_name(), data=cls.RUNTIME_PROGRAM, - metadata=cls.RUNTIME_PROGRAM_METADATA) + metadata=metadata) except RuntimeDuplicateProgramError: pass except IBMQNotAuthorizedError: @@ -199,13 +201,6 @@ def test_set_visibility(self): # Verify changed self.assertNotEqual(start_vis, end_vis) - def test_upload_program_conflict(self): - """Test uploading a program with conflicting name.""" - name = self._get_program_name() - self._upload_program(name=name) - with self.assertRaises(RuntimeDuplicateProgramError): - self._upload_program(name=name) - def test_delete_program(self): """Test deleting program.""" program_id = self._upload_program() @@ -625,7 +620,7 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) - self.assertTrue(program.version) + self.assertTrue(program.update_date) def _upload_program( self, @@ -636,13 +631,13 @@ def _upload_program( """Upload a new program.""" name = name or self._get_program_name() data = data or self.RUNTIME_PROGRAM + metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) + metadata['name'] = name + metadata['max_execution_time'] = max_execution_time + metadata['is_public'] = is_public program_id = self.provider.runtime.upload_program( - name=name, data=data, - is_public=is_public, - metadata=self.RUNTIME_PROGRAM_METADATA, - max_execution_time=max_execution_time, - description="Qiskit test program") + metadata=metadata) self.to_delete.append(program_id) return program_id