Skip to content

Commit c90f59d

Browse files
New function to negate expressions (#15447)
* added negation unary operator and implemented negation constructor for floats, int and delays * added documentation and tests * function and enum name changed, added release notes * linting and testing done, test fixed * added function name to the docstring * Uint no longer negatable, fixed the relevant tests and release notes * added negate to serialization stress-tests * added documentation, better example, fixed test_cases * linting and testing * added a more self contained example and updated release notes * added negate operation to necessary qasm3 files to fix printing issues * Fix a ruff issue * Fix docs failure in CI --------- Co-authored-by: Eli Arbel <arbel@il.ibm.com>
1 parent 2a1c299 commit c90f59d

9 files changed

Lines changed: 96 additions & 3 deletions

File tree

crates/circuit/src/classical/expr/unary.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ pub struct Unary {
4040
pub enum UnaryOp {
4141
BitNot = 1,
4242
LogicNot = 2,
43+
Negate = 3,
4344
}
4445

4546
unsafe impl ::bytemuck::CheckedBitPattern for UnaryOp {
4647
type Bits = u8;
4748

4849
fn is_valid_bit_pattern(bits: &Self::Bits) -> bool {
49-
*bits > 0 && *bits < 3
50+
*bits > 0 && *bits < 4
5051
}
5152
}
5253

qiskit/circuit/classical/expr/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
124124
.. autofunction:: bit_not
125125
.. autofunction:: logic_not
126+
.. autofunction:: negate
126127
127128
Similarly, the binary operations and relations have helper functions defined.
128129
@@ -230,6 +231,7 @@
230231
"logic_not",
231232
"logic_or",
232233
"mul",
234+
"negate",
233235
"not_equal",
234236
"shift_left",
235237
"shift_right",
@@ -263,4 +265,5 @@
263265
mul,
264266
div,
265267
lift_legacy_condition,
268+
negate,
266269
)

qiskit/circuit/classical/expr/constructors.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,3 +761,33 @@ def div(left: typing.Any, right: typing.Any) -> Expr:
761761
right,
762762
type,
763763
)
764+
765+
766+
def negate(operand: typing.Any, /) -> Expr:
767+
"""Negate an expression node from the given value, resolving any implicit casts and
768+
lifting the value into a :class:`Value` node if required.
769+
770+
Examples:
771+
Negation of a floating point number::
772+
773+
>>> from qiskit.circuit.classical import expr
774+
>>> expr.negate(5.0)
775+
Unary(\
776+
Unary.Op.NEGATE, \
777+
Value(5.0, Float()), \
778+
Float())
779+
780+
Negation of a duration::
781+
782+
>>> from qiskit.circuit import Duration
783+
>>> from qiskit.circuit.classical import expr
784+
>>> expr.negate(Duration.dt(1000))
785+
Unary(\
786+
Unary.Op.NEGATE, \
787+
Value(Duration.dt(1000), Duration()), \
788+
Duration())
789+
"""
790+
operand = lift(operand)
791+
if operand.type.kind not in (types.Float, types.Duration):
792+
raise TypeError(f"cannot apply '{Unary.Op.NEGATE}' to type '{operand.type}'")
793+
return Unary(Unary.Op.NEGATE, operand, operand.type)

qiskit/circuit/classical/expr/expr.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class _UnaryOp(enum.Enum):
4646
4747
The logical negation :data:`LOGIC_NOT` takes an input that is implicitly coerced to a
4848
Boolean, and returns a Boolean.
49+
50+
The arithmetic negation :data:`NEGATE` takes an input that is either float or duration and
51+
returns a value of the same type.
4952
"""
5053

5154
# If adding opcodes, remember to add helper constructor functions in `constructors.py`.
@@ -57,6 +60,8 @@ class _UnaryOp(enum.Enum):
5760
"""Bitwise negation. ``~operand``."""
5861
LOGIC_NOT = 2
5962
"""Logical negation. ``!operand``."""
63+
NEGATE = 3
64+
"""Arithmetic negation. ``-operand``."""
6065

6166
def __str__(self):
6267
return f"Unary.{super().__str__()}"

qiskit/qasm3/ast.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ class Unary(Expression):
306306
class Op(enum.Enum):
307307
LOGIC_NOT = "!"
308308
BIT_NOT = "~"
309+
NEGATE = "-"
309310

310311
def __init__(self, op: Op, operand: Expression):
311312
self.op = op

qiskit/qasm3/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#
3939
ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22),
4040
ast.Unary.Op.BIT_NOT: _BindingPower(right=22),
41+
ast.Unary.Op.NEGATE: _BindingPower(right=22),
4142
#
4243
# Modulo: (19, 20)
4344
ast.Binary.Op.MUL: _BindingPower(19, 20),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
features_circuits:
2+
- |
3+
Introduced a new unary negation operation :func:`~.expr.negate`, allowing users to negate
4+
float and duration expressions. In-case a scalar is passed, it is automatically lifted to
5+
the correct type.
6+
7+
Example usage::
8+
9+
from qiskit.circuit import ClassicalRegister
10+
from qiskit.circuit.classical import expr, types
11+
12+
qc = QuantumCircuit(2, 2)
13+
in_var1 = qc.add_input("in1", types.Float())
14+
15+
with qc.if_test(expr.greater(in_var1, 0.2)) as else_:
16+
qc.rz(0.3, 0)
17+
with else_:
18+
with qc.if_test(expr.greater(expr.negate(in_var1), 0.2)):
19+
qc.rz(-0.3, 0)

test/python/circuit/classical/test_expr_constructors.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def test_cast_allows_lossy_downcasting(self):
122122
(expr.logic_not, ClassicalRegister(3)),
123123
(expr.logic_not, False),
124124
(expr.logic_not, Clbit()),
125+
(expr.negate, 7.0),
126+
(expr.negate, Duration.dt(1000)),
125127
)
126128
@ddt.unpack
127129
def test_unary_functions_lift_scalars(self, function, scalar):
@@ -916,3 +918,34 @@ def test_div_forbidden(self):
916918
expr.div(255.0, 1)
917919
with self.assertRaisesRegex(TypeError, "invalid types"):
918920
expr.div(255.0, Duration.dt(1000))
921+
922+
def test_unary_negate_forbidden(self):
923+
with self.assertRaisesRegex(TypeError, "cannot apply"):
924+
expr.negate(True)
925+
with self.assertRaisesRegex(TypeError, "cannot apply"):
926+
expr.negate(Clbit())
927+
with self.assertRaisesRegex(TypeError, "cannot apply"):
928+
cr = ClassicalRegister(3)
929+
expr.negate(cr)
930+
with self.assertRaisesRegex(TypeError, "cannot apply"):
931+
expr.negate(expr.Value(5, types.Uint(3)))
932+
933+
def test_unary_negate_explicit(self):
934+
935+
self.assertEqual(
936+
expr.negate(7.0),
937+
expr.Unary(
938+
expr.Unary.Op.NEGATE,
939+
expr.Value(7.0, types.Float()),
940+
types.Float(),
941+
),
942+
)
943+
944+
self.assertEqual(
945+
expr.negate(Duration.dt(1000)),
946+
expr.Unary(
947+
expr.Unary.Op.NEGATE,
948+
expr.Value(Duration.dt(1000), types.Duration()),
949+
types.Duration(),
950+
),
951+
)

test/python/circuit/test_circuit_load_from_qpy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,7 +1774,7 @@ def test_if_else_expr_stress(self):
17741774
),
17751775
),
17761776
),
1777-
expr.logic_not(loose),
1777+
expr.greater(expr.negate(expr.cast(loose, types.Float())), 1.5),
17781778
),
17791779
outer.copy(),
17801780
[1],
@@ -1864,7 +1864,7 @@ def test_switch_expr_stress(self):
18641864
),
18651865
),
18661866
),
1867-
expr.logic_not(loose),
1867+
expr.greater(expr.negate(expr.cast(loose, types.Float())), 1.5),
18681868
),
18691869
[(False, outer.copy())],
18701870
[1],

0 commit comments

Comments
 (0)