Skip to content

Commit

Permalink
Verify values of parametrized rzz gates (#2021)
Browse files Browse the repository at this point in the history
* Verify values of parametrized rzz gates

* black

* lint

* black

* lint

* lint

* mypy

* mypy

* fix

* fix

* fix

* bug fix

* updated the parametrized angle test

* fixes

* adding a test

* test_rzz_recursive

* use assertRaisesRegex to refine assertions

* black

* lint

* removed a redundant test

* skip parameter expression

* black

* check fixed angles also in is_isa_circuit

* black

* removed extra white space in error message

* Update qiskit_ibm_runtime/utils/utils.py

Co-authored-by: Takashi Imamichi <[email protected]>

* spell fix

* Update qiskit_ibm_runtime/utils/utils.py

Co-authored-by: Ian Hincks <[email protected]>

* Update qiskit_ibm_runtime/utils/utils.py

Co-authored-by: Ian Hincks <[email protected]>

* release notes

* black

* changed tolerated rounding error

* lint

* changed wording in the release note

* Update release-notes/unreleased/2021.feat.rst

Co-authored-by: Will Shanks <[email protected]>

* renamed functions

* revised a comment

---------

Co-authored-by: Takashi Imamichi <[email protected]>
Co-authored-by: Ian Hincks <[email protected]>
Co-authored-by: Will Shanks <[email protected]>
Co-authored-by: Will Shanks <[email protected]>
Co-authored-by: Kevin Tian <[email protected]>
  • Loading branch information
6 people authored Dec 3, 2024
1 parent 92ee498 commit 681aeb9
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 13 deletions.
4 changes: 4 additions & 0 deletions qiskit_ibm_runtime/base_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
from .options.utils import merge_options_v2
from .runtime_job_v2 import RuntimeJobV2
from .ibm_backend import IBMBackend

from .utils import (
validate_isa_circuits,
validate_no_dd_with_dynamic_circuits,
validate_rzz_pubs,
validate_no_param_expressions_gen3_runtime,
)
from .utils.default_session import get_cm_session
Expand Down Expand Up @@ -153,6 +155,8 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV
validate_no_dd_with_dynamic_circuits([pub.circuit for pub in pubs], self.options)
validate_no_param_expressions_gen3_runtime([pub.circuit for pub in pubs], self.options)
if self._backend:
if not is_simulator(self._backend):
validate_rzz_pubs(pubs)
for pub in pubs:
if getattr(self._backend, "target", None) and not is_simulator(self._backend):
validate_isa_circuits([pub.circuit], self._backend.target)
Expand Down
1 change: 1 addition & 0 deletions qiskit_ibm_runtime/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
validate_no_dd_with_dynamic_circuits,
validate_isa_circuits,
validate_job_tags,
validate_rzz_pubs,
validate_no_param_expressions_gen3_runtime,
)

Expand Down
101 changes: 99 additions & 2 deletions qiskit_ibm_runtime/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import re
from queue import Queue
from threading import Condition
from typing import List, Optional, Any, Dict, Union, Tuple
from typing import List, Optional, Any, Dict, Union, Tuple, Set
from urllib.parse import urlparse
from itertools import chain
import numpy as np

import requests
Expand All @@ -39,6 +40,9 @@
)
from qiskit.transpiler import Target
from qiskit.providers.backend import BackendV1, BackendV2
from qiskit.primitives.containers.estimator_pub import EstimatorPub
from qiskit.primitives.containers.sampler_pub import SamplerPub

from .deprecation import deprecate_function


Expand Down Expand Up @@ -82,7 +86,7 @@ def _is_isa_circuit_helper(circuit: QuantumCircuit, target: Target, qubit_map: D
if (
name == "rzz"
and not isinstance((param := instruction.operation.params[0]), ParameterExpression)
and (param < 0.0 or param > 1.001 * np.pi / 2)
and (param < 0.0 or param > np.pi / 2 + 1e-10)
):
return (
f"The instruction {name} on qubits {qargs} is supported only for angles in the "
Expand Down Expand Up @@ -123,6 +127,99 @@ def is_isa_circuit(circuit: QuantumCircuit, target: Target) -> str:
return _is_isa_circuit_helper(circuit, target, qubit_map)


def _is_valid_rzz_pub_helper(circuit: QuantumCircuit) -> Union[str, Set[Parameter]]:
"""
For rzz gates:
- Verify that numeric angles are in the range [0, pi/2]
- Collect parameterized angles
Returns one of the following:
- A string, containing an error message, if a numeric angle is outside of the range [0, pi/2]
- A list of names of all the parameters that participate in an rzz gate
Note: we check for parametrized rzz gates inside control flow operation, although fractional
gates are actually impossible in combination with dynamic circuits. This is in order to remain
correct if this restriction is removed at some point.
"""
angle_params = set()

for instruction in circuit.data:
operation = instruction.operation

# rzz gate is calibrated only for the range [0, pi/2].
# We allow an angle value of a bit more than pi/2, to compensate floating point rounding
# errors (beyond pi/2 does not trigger an error down the stack, only may become less
# accurate).
if operation.name == "rzz":
angle = instruction.operation.params[0]
if isinstance(angle, Parameter):
angle_params.add(angle.name)
elif not isinstance(angle, ParameterExpression) and (
angle < 0.0 or angle > np.pi / 2 + 1e-10
):
return (
"The instruction rzz is supported only for angles in the "
f"range [0, pi/2], but an angle of {angle} has been provided."
)

if isinstance(operation, ControlFlowOp):
for sub_circ in operation.blocks:
body_result = _is_valid_rzz_pub_helper(sub_circ)
if isinstance(body_result, str):
return body_result
angle_params.update(body_result)

return angle_params


def is_valid_rzz_pub(pub: Union[EstimatorPub, SamplerPub]) -> str:
"""Verify that all rzz angles are in the range [0, pi/2].
Args:
pub: A pub to be checked
Returns:
An empty string if all angles are valid, otherwise an error message.
"""
helper_result = _is_valid_rzz_pub_helper(pub.circuit)

if isinstance(helper_result, str):
return helper_result

if len(helper_result) == 0:
return ""

# helper_result is a set of parameter names
rzz_params = list(helper_result)

# gather all parameter names, in order
pub_params = list(chain.from_iterable(pub.parameter_values.data))

col_indices = np.where(np.isin(pub_params, rzz_params))[0]
# col_indices is the indices of columns in the parameter value array that have to be checked

# first axis will be over flattened shape, second axis over circuit parameters
arr = pub.parameter_values.ravel().as_array()

# project only to the parameters that have to be checked
arr = arr[:, col_indices]

# We allow an angle value of a bit more than pi/2, to compensate floating point rounding
# errors (beyond pi/2 does not trigger an error down the stack, only may become less
# accurate).
bad = np.where((arr < 0.0) | (arr > np.pi / 2 + 1e-10))

# `bad` is a tuple of two arrays, which can be empty, like this:
# (array([], dtype=int64), array([], dtype=int64))
if len(bad[0]) > 0:
return (
f"Assignment of value {arr[bad[0][0], bad[1][0]]} to Parameter "
f"'{pub_params[col_indices[bad[1][0]]]}' is an invalid angle for the rzz gate"
)

return ""


def are_circuits_dynamic(circuits: List[QuantumCircuit], qasm_default: bool = True) -> bool:
"""Checks if the input circuits are dynamic."""
for circuit in circuits:
Expand Down
15 changes: 14 additions & 1 deletion qiskit_ibm_runtime/utils/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# that they have been altered from the originals.

"""Utilities for data validation."""
from typing import List, Sequence, Optional, Any
from typing import List, Sequence, Optional, Any, Union
import warnings
import keyword

Expand All @@ -22,6 +22,7 @@
from qiskit_ibm_runtime.utils.utils import (
is_isa_circuit,
are_circuits_dynamic,
is_valid_rzz_pub,
has_param_expressions,
)
from qiskit_ibm_runtime.exceptions import IBMInputValueError
Expand Down Expand Up @@ -102,6 +103,18 @@ def validate_isa_circuits(circuits: Sequence[QuantumCircuit], target: Target) ->
)


def validate_rzz_pubs(pubs: Union[List[EstimatorPub], List[SamplerPub]]) -> None:
"""Validate that rzz angles are always in the range [0, pi/2]
Args:
pubs: A list of pubs.
"""
for pub in pubs:
message = is_valid_rzz_pub(pub)
if message:
raise IBMInputValueError(message)


def validate_no_dd_with_dynamic_circuits(circuits: List[QuantumCircuit], options: Any) -> None:
"""Validate that if dynamical decoupling options are enabled,
no circuit in the pubs is dynamic
Expand Down
1 change: 1 addition & 0 deletions release-notes/unreleased/2021.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A new function ``validate_rzz_pubs`` was added. The function verifies that ``rzz`` parameters are in the range between ``0`` and ``pi/2``, for numeric parameters (e.g. ``rzz(np.pi/4, 0)``), and for unbounded parameters (``rzz(theta, 0)``) with values to substitute provided in the pub. Parameter expressions (e.g. ``rzz(theta + phi, 0)``) are still not validated.
98 changes: 88 additions & 10 deletions test/unit/test_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def test_isa_inside_condition_block_body_in_separate_circuit(self, backend):
SamplerV2(backend).run(pubs=[(circ)])

@data(-1, 1, 2)
def test_rzz_angle_validation(self, angle):
def test_rzz_fixed_angle_validation(self, angle):
"""Test exception when rzz gate is used with an angle outside the range [0, pi/2]"""
backend = FakeFractionalBackend()

Expand All @@ -296,12 +296,13 @@ def test_rzz_angle_validation(self, angle):
if angle == 1:
SamplerV2(backend).run(pubs=[(circ)])
else:
with self.assertRaises(IBMInputValueError):
with self.assertRaisesRegex(IBMInputValueError, f"{angle}"):
SamplerV2(backend).run(pubs=[(circ)])

def test_rzz_validates_only_for_fixed_angles(self):
"""Verify that the rzz validation occurs only when the angle is a number, and not a
parameter"""
@data(-1, 1, 2)
def test_rzz_parametrized_angle_validation(self, angle):
"""Test exception when rzz gate is used with a parameter which is assigned a value outside
the range [0, pi/2]"""
backend = FakeFractionalBackend()
param = Parameter("p")

Expand All @@ -311,11 +312,88 @@ def test_rzz_validates_only_for_fixed_angles(self):
# Should run without an error
SamplerV2(backend).run(pubs=[(circ, [1])])

with self.subTest("parameter expression"):
circ = QuantumCircuit(2)
circ.rzz(2 * param, 0, 1)
# Should run without an error
SamplerV2(backend).run(pubs=[(circ, [0.5])])
if angle == 1:
SamplerV2(backend).run(pubs=[(circ, [angle])])
else:
with self.assertRaisesRegex(IBMInputValueError, f"{angle}.*Parameter 'p'"):
SamplerV2(backend).run(pubs=[(circ, [angle])])

@data(("a", -1), ("b", 2), ("d", 3), (-1, 1), (1, 2), None)
def test_rzz_complex(self, flawed_params):
"""Testing rzz validation in the currently non-existing case of dynamic instructions"""
# pylint: disable=not-context-manager

# FakeFractionalBackend has both fractional and dynamic instructions
backend = FakeFractionalBackend()

aparam = Parameter("a")
bparam = Parameter("b")
cparam = Parameter("c")
dparam = Parameter("d")

angle1 = 1
angle2 = 1
if flawed_params is not None and not isinstance(flawed_params[0], str):
angle1 = flawed_params[0]
angle2 = flawed_params[1]

circ = QuantumCircuit(2, 1)
circ.rzz(bparam, 0, 1)
circ.rzz(angle1, 0, 1)
circ.measure(0, 0)
with circ.if_test((0, 1)):
circ.rzz(aparam, 0, 1)
circ.rzz(angle2, 0, 1)
circ.rx(cparam, 0)
circ.rzz(dparam, 0, 1)
circ.rzz(1, 0, 1)
circ.rzz(aparam, 0, 1)

val_ab = np.ones([2, 2, 3, 2])
val_c = (-1) * np.ones([2, 2, 3])
val_d = np.ones([2, 2, 3])

if flawed_params is not None and isinstance(flawed_params[0], str):
if flawed_params[0] == "a":
val_ab[0, 1, 1, 0] = flawed_params[1]
val_ab[1, 0, 2, 1] = flawed_params[1]
if flawed_params[0] == "b":
val_ab[1, 0, 2, 1] = flawed_params[1]
val_d[1, 1, 1] = flawed_params[1]
if flawed_params[0] == "d":
val_d[1, 1, 1] = flawed_params[1]
val_ab[1, 1, 2, 1] = flawed_params[1]

pub = (circ, {("a", "b"): val_ab, "c": val_c, "d": val_d})

if flawed_params is None:
SamplerV2(backend).run(pubs=[pub])
else:
if isinstance(flawed_params[0], str):
with self.assertRaisesRegex(
IBMInputValueError, f"{flawed_params[1]}.*Parameter '{flawed_params[0]}'"
):
SamplerV2(backend).run(pubs=[pub])
else:
with self.assertRaisesRegex(
IBMInputValueError, f"{flawed_params[0] * flawed_params[1]}"
):
SamplerV2(backend).run(pubs=[pub])

def test_rzz_validation_skips_param_exp(self):
"""Verify that the rzz validation occurs only when the angle is a number or a parameter,
but not a parameter expression"""
backend = FakeFractionalBackend()
param = Parameter("p")

circ = QuantumCircuit(2)
circ.rzz(2 * param, 0, 1)

# Since we currently don't validate parameter expressions, the following line should run
# without an error, in spite of the angle being larger than pi/2
# (if there is an error, it is an expected one, such as a parameter expression being
# treated as if it were a float)
SamplerV2(backend).run(pubs=[(circ, [1])])

def test_param_expressions_gen3_runtime(self):
"""Verify that parameter expressions are not used in combination with the gen3-turbo
Expand Down

0 comments on commit 681aeb9

Please sign in to comment.