Skip to content
This repository was archived by the owner on Jul 28, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions qiskit/providers/ibmq/api/clients/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Client for accessing IBM Quantum runtime service."""

import logging
from typing import Any, List, Dict, Union, Optional
from typing import Any, Dict, Optional

from qiskit.providers.ibmq.credentials import Credentials
from qiskit.providers.ibmq.api.session import RetrySession
Expand Down Expand Up @@ -111,7 +111,8 @@ def program_run(
credentials: Credentials,
backend_name: str,
params: Dict,
image: str
image: str,
log_level: Optional[str] = None
) -> Dict:
"""Run the specified program.

Expand All @@ -121,14 +122,15 @@ def program_run(
backend_name: Name of the backend to run the program.
params: Parameters to use.
image: The runtime image to use.
log_level: Log level to use.

Returns:
JSON response.
"""
return self.api.program_run(program_id=program_id, hub=credentials.hub,
group=credentials.group, project=credentials.project,
backend_name=backend_name, params=params,
image=image)
image=image, log_level=log_level)

def program_delete(self, program_id: str) -> None:
"""Delete the specified program.
Expand Down
8 changes: 6 additions & 2 deletions qiskit/providers/ibmq/api/rest/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Runtime REST adapter."""

import logging
from typing import Dict, List, Any, Union, Optional
from typing import Dict, Any, Union, Optional
import json
from concurrent import futures

Expand Down Expand Up @@ -114,7 +114,8 @@ def program_run(
project: str,
backend_name: str,
params: Dict,
image: str
image: str,
log_level: Optional[str] = None
) -> Dict:
"""Execute the program.

Expand All @@ -126,6 +127,7 @@ def program_run(
backend_name: Name of the backend.
params: Program parameters.
image: Runtime image.
log_level: Log level to use.

Returns:
JSON response.
Expand All @@ -140,6 +142,8 @@ def program_run(
'params': params,
'runtime': image
}
if log_level:
payload["log_level"] = log_level
data = json.dumps(payload, cls=RuntimeEncoder)
return self.session.post(url, data=data).json()

Expand Down
2 changes: 2 additions & 0 deletions qiskit/providers/ibmq/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,13 @@ def interim_result_callback(job_id, interim_result):
RuntimeEncoder
RuntimeDecoder
ParameterNamespace
RuntimeOptions
"""

from .ibm_runtime_service import IBMRuntimeService
from .runtime_job import RuntimeJob
from .runtime_program import RuntimeProgram, ParameterNamespace
from .runtime_options import RuntimeOptions
from .program.user_messenger import UserMessenger
from .program.program_backend import ProgramBackend
from .program.result_decoder import ResultDecoder
Expand Down
35 changes: 21 additions & 14 deletions qiskit/providers/ibmq/runtime/ibm_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import logging
from typing import Dict, Callable, Optional, Union, List, Any, Type
import json
import re
import warnings

from qiskit.providers.exceptions import QiskitBackendNotFoundError
Expand All @@ -27,6 +26,7 @@
from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound,
RuntimeJobNotFound)
from .program.result_decoder import ResultDecoder
from .runtime_options import RuntimeOptions
from ..api.clients.runtime import RuntimeClient

from ..api.exceptions import RequestsApiError
Expand Down Expand Up @@ -232,7 +232,7 @@ def _to_program(self, response: Dict) -> RuntimeProgram:
def run(
self,
program_id: str,
options: Dict,
options: Union[RuntimeOptions, Dict],
inputs: Union[Dict, ParameterNamespace],
callback: Optional[Callable] = None,
result_decoder: Optional[Type[ResultDecoder]] = None,
Expand All @@ -242,8 +242,9 @@ def run(

Args:
program_id: Program ID.
options: Runtime options that control the execution environment.
Currently the only available option is ``backend_name``, which is required.
options: Runtime options that control the execution environment. See
:class:`RuntimeOptions` for all available options.
Currently the only required option is ``backend_name``.
inputs: Program input parameters. These input values are passed
to the runtime program.
callback: Callback function to be invoked for any interim results.
Expand All @@ -263,25 +264,31 @@ def run(
Raises:
IBMQInputValueError: If input is invalid.
"""
if 'backend_name' not in options:
raise IBMQInputValueError('"backend_name" is required field in "options"')
if isinstance(options, dict):
options = RuntimeOptions(**options)

if image:
warnings.warn("Passing the 'image' keyword to IBMRuntimeService.run is "
"deprecated and will be removed in a future release. "
"Please pass it in as part of 'options'.",
DeprecationWarning, stacklevel=2)
options.image = image

options.validate()

# If using params object, extract as dictionary
if isinstance(inputs, ParameterNamespace):
inputs.validate()
inputs = vars(inputs)

if image and not \
re.match("[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$",
image):
raise IBMQInputValueError('"image" needs to be in form of image_name:tag')

backend_name = options['backend_name']
backend_name = options.backend_name
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=inputs,
image=image)
image=options.image,
log_level=options.log_level)

backend = self._provider.get_backend(backend_name)
job = RuntimeJob(backend=backend,
Expand All @@ -290,7 +297,7 @@ def run(
job_id=response['id'], program_id=program_id, params=inputs,
user_callback=callback,
result_decoder=result_decoder,
image=image)
image=options.image)
return job

def upload_program(
Expand Down
64 changes: 64 additions & 0 deletions qiskit/providers/ibmq/runtime/runtime_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# 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.

"""Runtime options that control the execution environment."""

import re
import logging
from dataclasses import dataclass
from typing import Optional

from ..exceptions import IBMQInputValueError


@dataclass

@rathishcholarajan rathishcholarajan Feb 2, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see dataclass is a Python 3.7+ feature. We still support Python 3.6 for the upcoming provider release since terra 0.19.x still supports it and then it will be dropped after the 0.19.0 provider release. Would this work with Python 3.6?

@jyu00 jyu00 Feb 2, 2022

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But Python 3.6 end of life was more than a month ago, and the latest qiskit-terra no longer supports 3.6.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just add dataclasses>=0.8;python_version<'3.7' to the requirements list. Being in the metapackage puts extra requirements and for the metapackage we've got more conservative support versions and we've previously advertised that terra 0.19.x (and aer 0.10.x) supports python 3.6. So we can't add anything to the metapackage that requires >3.6 until we release terra 0.20.0

class RuntimeOptions:
"""Class for representing runtime execution options.

Args:
backend_name: target backend to run on. This is required.
image: the runtime image used to execute the program, specified in
the form of ``image_name:tag``. Not all accounts are
authorized to select a different image.
log_level: logging level to set in the execution environment. The valid
log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
The default level is ``WARNING``.
"""

backend_name: str = None
image: Optional[str] = None
log_level: Optional[str] = None

def validate(self) -> None:
"""Validate options.

Raises:
IBMQInputValueError: If one or more option is invalid.
"""
if self.image and not re.match(
"[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$",
self.image,
):
raise IBMQInputValueError('"image" needs to be in form of image_name:tag')

if not self.backend_name:
raise IBMQInputValueError(
'"backend_name" is required field in "options".'
)

if self.log_level and not isinstance(
logging.getLevelName(self.log_level.upper()), int
):
raise IBMQInputValueError(
f"{self.log_level} is not a valid log level. The valid log levels are: `DEBUG`, "
f"`INFO`, `WARNING`, `ERROR`, and `CRITICAL`."
)
12 changes: 12 additions & 0 deletions releasenotes/notes/logging-level-c64f05bdb36c0685.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
features:
- |
You can now specify a different logging level in the ``options`` keyword
when submitting a Qiskit Runtime job with the
:meth:`qiskit.providers.ibmq.runtime.IBMRuntimeService.run` method.
deprecations:
- |
The ``image`` keyword in the
:meth:`qiskit.providers.ibmq.runtime.IBMRuntimeService.run` method is
deprecated. You should instead specify the image to use in the ``options``
keyword.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ python-dateutil>=2.8.0
websocket-client>=1.0.1
websockets>=10.0; python_version >= '3.7'
websockets>=9.1; python_version < '3.7'
dataclasses>=0.8; python_version < '3.7'
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"python-dateutil>=2.8.0",
"websocket-client>=1.0.1",
"websockets>=10.0 ; python_version>='3.7'",
"websockets>=9.1 ; python_version<'3.7'"
"websockets>=9.1 ; python_version<'3.7'",
"dataclasses>=0.8 ; python_version<'3.7'"
]

# Handle version.
Expand Down
7 changes: 5 additions & 2 deletions test/ibmq/runtime/fake_runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class BaseFakeRuntimeJob:
_executor = ThreadPoolExecutor() # pylint: disable=bad-option-value,consider-using-with

def __init__(self, job_id, program_id, hub, group, project, backend_name, final_status,
params, image):
params, image, log_level=None):
"""Initialize a fake job."""
self._job_id = job_id
self._status = final_status or "QUEUED"
Expand All @@ -89,6 +89,7 @@ def __init__(self, job_id, program_id, hub, group, project, backend_name, final_
self._backend_name = backend_name
self._params = params
self._image = image
self.log_level = log_level
if final_status is None:
self._future = self._executor.submit(self._auto_progress)
self._result = None
Expand Down Expand Up @@ -292,7 +293,8 @@ def program_run(
credentials: Credentials,
backend_name: str,
params: str,
image: Optional[str] = ""
image: Optional[str] = "",
log_level: Optional[str] = "WARNING"
):
"""Run the specified program."""
job_id = uuid.uuid4().hex
Expand All @@ -301,6 +303,7 @@ def program_run(
hub=credentials.hub, group=credentials.group,
project=credentials.project, backend_name=backend_name,
params=params, final_status=self._final_status, image=image,
log_level=log_level,
**self._job_kwargs)
self._jobs[job_id] = job
return {'id': job_id}
Expand Down
10 changes: 9 additions & 1 deletion test/ibmq/runtime/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ def test_run_program_with_custom_runtime_image(self):
self.assertTrue(job.result())
self.assertEqual(job.image, image)

def test_run_program_with_custom_log_level(self):
"""Test running program with a custom log level."""
job = self._run_program(log_level="DEBUG")
job_raw = self.runtime._api_client._get_job(job.job_id())
self.assertEqual(job_raw.log_level, "DEBUG")

def test_retrieve_program_data(self):
"""Test retrieving program data"""
program_id = self._upload_program(name="qiskit-test")
Expand Down Expand Up @@ -739,9 +745,11 @@ def _upload_program(self, name=None, max_execution_time=300,
return program_id

def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None,
decoder=None, image=""):
decoder=None, image="", log_level=None):
"""Run a program."""
options = {'backend_name': "some_backend"}
if log_level:
options["log_level"] = log_level
if final_status is not None:
self.runtime._api_client.set_final_status(final_status)
elif job_classes:
Expand Down
22 changes: 21 additions & 1 deletion test/ibmq/runtime/test_runtime_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ class TestRuntimeIntegration(IBMQTestCase):
import random
import time
import warnings
import logging

from qiskit import transpile
from qiskit.circuit.random import random_circuit

logger = logging.getLogger("qiskit-test")

def prepare_circuits(backend):
circuit = random_circuit(num_qubits=5, depth=4, measure=True,
seed=random.randint(0, 1000))
Expand All @@ -68,6 +71,7 @@ def main(backend, user_messenger, **kwargs):
user_messenger.publish(final_result, final=True)
print("this is a stdout message")
warnings.warn("this is a stderr message")
logger.info("this is an info log")
"""

RUNTIME_PROGRAM_METADATA = {
Expand Down Expand Up @@ -667,6 +671,20 @@ def test_job_logs(self):
self.assertIn("this is a stdout message", job_logs)
self.assertIn("this is a stderr message", job_logs)

def test_run_program_log_level(self):
"""Test running with a custom log level."""
levels = ["INFO", "ERROR"]
for level in levels:
with self.subTest(level=level):
job = self._run_program(log_level=level)
job.wait_for_final_state()
expect_info_msg = level == "INFO"
self.assertEqual(
"info log" in job.logs(),
expect_info_msg,
f"Job log is {job.logs()}",
)

def _validate_program(self, program):
"""Validate a program."""
self.assertTrue(program)
Expand Down Expand Up @@ -710,13 +728,15 @@ def _assert_complex_types_equal(self, expected, received):

def _run_program(self, program_id=None, iterations=1,
interim_results=None, final_result=None,
callback=None):
callback=None, log_level=None):
"""Run a program."""
inputs = {'iterations': iterations,
'interim_results': interim_results or {},
'final_result': final_result or {}}
pid = program_id or self.program_id
options = {'backend_name': self.backend.name()}
if log_level:
options["log_level"] = log_level
job = self.provider.runtime.run(program_id=pid, inputs=inputs,
options=options, callback=callback)
self.log.info("Runtime job %s submitted.", job.job_id())
Expand Down