Skip to content

Commit

Permalink
Refactoring; new boolean expression code moved to synthesis, old code…
Browse files Browse the repository at this point in the history
… restored.
  • Loading branch information
gadial committed Feb 6, 2025
1 parent 2651617 commit e403028
Show file tree
Hide file tree
Showing 15 changed files with 620 additions and 244 deletions.
182 changes: 63 additions & 119 deletions qiskit/circuit/classicalfunction/boolean_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@

"""A quantum oracle constructed from a logical expression or a string in the DIMACS format."""

import ast
import itertools
import re
from os.path import basename, isfile
from typing import Callable, Optional

from qiskit.circuit import Gate
from .boolean_expression_visitor import (
BooleanExpressionEvalVisitor,
BooleanExpressionArgsCollectorVisitor,
)
from qiskit.circuit import QuantumCircuit
from qiskit.utils.optionals import HAS_TWEEDLEDUM
from .classical_element import ClassicalElement


class BooleanExpression(Gate):
@HAS_TWEEDLEDUM.require_in_instance
class BooleanExpression(ClassicalElement):
"""The Boolean Expression gate."""

def __init__(self, expression: str, name: str = None, var_order: list = None) -> None:
Expand All @@ -36,20 +33,18 @@ def __init__(self, expression: str, name: str = None, var_order: list = None) ->
var_order(list): A list with the order in which variables will be created.
(default: by appearance)
"""
self.expression = expression
self.expression_ast = ast.parse(expression)

from tweedledum import BoolFunction # pylint: disable=import-error

self._tweedledum_bool_expression = BoolFunction.from_expression(
expression, var_order=var_order
)

short_expr_for_name = (expression[:10] + "...") if len(expression) > 13 else expression
args_collector = BooleanExpressionArgsCollectorVisitor()
args_collector.visit(self.expression_ast)
self.args = args_collector.get_sorted_args()
if var_order is not None:
missing_args = set(self.args) - set(var_order)
if len(missing_args) > 0:
raise ValueError(f"var_order missing the variable(s) {', '.join(missing_args)}")
self.args.sort(key=var_order.index)

num_qubits = len(self.args) + 1 # one for output qubit
self.definition = None
num_qubits = (
self._tweedledum_bool_expression.num_outputs()
+ self._tweedledum_bool_expression.num_inputs()
)
super().__init__(name or short_expr_for_name, num_qubits=num_qubits, params=[])

def simulate(self, bitstring: str) -> bool:
Expand All @@ -63,112 +58,49 @@ def simulate(self, bitstring: str) -> bool:
Returns:
bool: result of the evaluation.
"""
eval_visitor = BooleanExpressionEvalVisitor()
if len(self.args) != len(bitstring):
raise ValueError(
f"bitstring length differs from the number of variables "
f"({len(bitstring)} != {len(self.args)})"
)
for arg, bit in zip(self.args, bitstring):
if not bit in ["0", "1"]:
raise ValueError("bitstring must be composed of 0 and 1 only")
eval_visitor.arg_values[arg] = bit == "1"
return eval_visitor.visit(self.expression_ast)

def truth_table(self) -> dict:
"""Generates the full truth table for the expression
Returns:
dict: A dictionary mapping boolean assignments to the boolean result
"""
return {
assignment: self.simulate("".join("1" if val else "0" for val in assignment))
for assignment in itertools.product([False, True], repeat=len(self.args))
}

def synth(self, circuit_type: str = "bit"):
r"""Synthesize the logic network into a :class:`~qiskit.circuit.QuantumCircuit`.
There are two common types of circuits for a boolean function :math:`f(x)`:
from tweedledum import BitVec # pylint: disable=import-error

1. **Bit-flip oracles** which compute:
bits = []
for bit in bitstring:
bits.append(BitVec(1, bit))
return bool(self._tweedledum_bool_expression.simulate(*bits))

.. math::
|x\rangle|y\rangle |-> |x\rangle|f(x)\oplusy\rangle
2. **Phase-flip** oracles which compute:
.. math::
|x\rangle |-> (-1)^{f(x)}|x\rangle
By default the bit-flip oracle is generated.
def synth(
self,
registerless: bool = True,
synthesizer: Optional[Callable[["BooleanExpression"], QuantumCircuit]] = None,
):
"""Synthesis the logic network into a :class:`~qiskit.circuit.QuantumCircuit`.
Args:
circuit_type: which type of oracle to create, 'bit' or 'phase' flip oracle.
registerless: Default ``True``. If ``False`` uses the parameter names
to create registers with those names. Otherwise, creates a circuit with a flat
quantum register.
synthesizer: A callable that takes self and returns a Tweedledum
circuit.
Returns:
QuantumCircuit: A circuit implementing the logic network.
Raises:
ValueError: If ``circuit_type`` is not either 'bit' or 'phase'.
"""
# pylint: disable=cyclic-import
from .boolean_expression_synth import (
synth_bit_oracle_from_esop,
synth_phase_oracle_from_esop,
EsopGenerator,
) # import here to avoid cyclic import

# generating the esop currntly requires generating the full truth table
# there are many optimizations that can be done to improve this step
esop = EsopGenerator(self.truth_table()).esop
if circuit_type == "bit":
return synth_bit_oracle_from_esop(esop)
if circuit_type == "phase":
return synth_phase_oracle_from_esop(esop)
raise ValueError("'circuit_type' must be either 'bit' or 'phase'")
if registerless:
qregs = None
else:
qregs = None # TODO: Probably from self._tweedledum_bool_expression._signature

if synthesizer is None:
from .utils import tweedledum2qiskit # Avoid an import cycle
from tweedledum.synthesis import pkrm_synth # pylint: disable=import-error

truth_table = self._tweedledum_bool_expression.truth_table(output_bit=0)
return tweedledum2qiskit(pkrm_synth(truth_table), name=self.name, qregs=qregs)
return synthesizer(self)

def _define(self):
"""The definition of the boolean expression is its synthesis"""
self.definition = self.synth()

@staticmethod
def from_dimacs(dimacs: str, name: str = None):
"""Create a BooleanExpression from a string in the DIMACS format.
Args:
dimacs : A string in DIMACS format.
name: an optional name for the BooleanExpression
Returns:
BooleanExpression: A gate for the input string
Raises:
ValueError: If the string is not formatted according to DIMACS rules
"""
header_regex = re.compile(r"p\s+cnf\s+(\d+)\s+(\d+)")
clause_regex = re.compile(r"(-?\d+)")
lines = [
line for line in dimacs.split("\n") if not line.startswith("c") and line != ""
] # DIMACS comment line start with c
header_match = header_regex.match(lines[0])
if not header_match:
raise ValueError("First line must start with 'p cnf'")
num_vars, _ = map(int, header_match.groups())
variables = [f"x{i+1}" for i in range(num_vars)]
clauses = []
for line in lines[1:]:
literals = clause_regex.findall(line)
if len(literals) == 0 or literals[-1] != "0":
continue
clauses.append([int(c) for c in literals[:-1]])
clause_strings = [
" | ".join([f'{"~" if lit < 0 else ""}{variables[abs(lit)-1]}' for lit in clause])
for clause in clauses
]
expr = " & ".join([f"({c})" for c in clause_strings])
return BooleanExpression(expr, name=name, var_order=variables)

@staticmethod
def from_dimacs_file(filename: str):
"""Create a BooleanExpression from a file in the DIMACS format.
@classmethod
def from_dimacs_file(cls, filename: str):
"""Create a BooleanExpression from the string in the DIMACS format.
Args:
filename: A file in DIMACS format.
Expand All @@ -178,8 +110,20 @@ def from_dimacs_file(filename: str):
Raises:
FileNotFoundError: If filename is not found.
"""
HAS_TWEEDLEDUM.require_now("BooleanExpression")

from tweedledum import BoolFunction # pylint: disable=import-error

expr_obj = cls.__new__(cls)
if not isfile(filename):
raise FileNotFoundError(f"The file {filename} does not exists.")
with open(filename, "r") as dimacs_file:
dimacs = dimacs_file.read()
return BooleanExpression.from_dimacs(dimacs, name=basename(filename))
expr_obj._tweedledum_bool_expression = BoolFunction.from_dimacs_file(filename)

num_qubits = (
expr_obj._tweedledum_bool_expression.num_inputs()
+ expr_obj._tweedledum_bool_expression.num_outputs()
)
super(BooleanExpression, expr_obj).__init__(
name=basename(filename), num_qubits=num_qubits, params=[]
)
return expr_obj
13 changes: 0 additions & 13 deletions qiskit/circuit/classicalfunction/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ class ClassicalFunctionCompilerError(QiskitError):
pass


class BooleanExpressionCompilerError(QiskitError):
"""BooleanExpression compiler generic error."""

pass


class ClassicalFunctionParseError(ClassicalFunctionCompilerError):
"""ClassicalFunction compiler parse error.
The classicalfunction function fails at parsing time."""
Expand All @@ -39,10 +33,3 @@ class ClassicalFunctionCompilerTypeError(ClassicalFunctionCompilerError):
The classicalfunction function fails at type checking time."""

pass


class BooleanExpressionParseError(BooleanExpressionCompilerError):
"""BooleanExpressionParseError compiler parse error.
The BooleanExpression function fails at parsing time."""

pass
2 changes: 2 additions & 0 deletions qiskit/circuit/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@
QuantumVolume
PhaseEstimation
GroverOperator
BitFlipOracle
PhaseOracle
PauliEvolutionGate
HamiltonianGate
Expand Down Expand Up @@ -671,5 +672,6 @@
from .phase_estimation import PhaseEstimation, phase_estimation
from .grover_operator import GroverOperator, grover_operator
from .phase_oracle import PhaseOracle
from .bit_flip_oracle import BitFlipOracle
from .overlap import UnitaryOverlap, unitary_overlap
from .standard_gates import get_standard_gate_name_mapping
127 changes: 127 additions & 0 deletions qiskit/circuit/library/bit_flip_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2025.
#
# 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.

"""Bit-flip Oracle object."""

from qiskit.circuit import QuantumCircuit

from qiskit.synthesis.boolean.boolean_expression import BooleanExpression


class BitFlipOracle(QuantumCircuit):
r"""Bit-flip Oracle.
The Bit-flip Oracle object constructs circuits for any arbitrary
input logical expressions. A logical expression is composed of logical operators
`&` (`AND`), `|` (`OR`), `~` (`NOT`), and `^` (`XOR`).
as well as symbols for literals (variables).
For example, `'a & b'`, and `(v0 | ~v1) & (~v2 & v3)`
are both valid string representation of boolean logical expressions.
A bit-flip oracle for a boolean function `f(x)` performs the following
quantum operation:
.. math::
|x\rangle|y\rangle \mapsto |x\rangle|f(x)\oplus y\rangle
For convenience, this oracle, in addition to parsing arbitrary logical expressions,
also supports input strings in the `DIMACS CNF format
<http://www.satcompetition.org/2009/format-benchmarks2009.html>`__,
which is the standard format for specifying SATisfiability (SAT) problem instances in
`Conjunctive Normal Form (CNF) <https://en.wikipedia.org/wiki/Conjunctive_normal_form>`__,
which is a conjunction of one or more clauses, where a clause is a disjunction of one
or more literals. See :meth:`qiskit.circuit.library.bit_flip_oracle.BitFlipOracle.from_dimacs_file`.
From 16 variables on, possible performance issues should be expected when using the
default synthesizer.
"""

def __init__(
self,
expression: str,
var_order: list = None,
) -> None:
"""Creates a BitFlipOracle object
Args:
expression: A Python-like boolean expression.
var_order(list): A list with the order in which variables will be created.
(default: by appearance)
"""
self.boolean_expression = BooleanExpression(expression, var_order=var_order)
oracle = self.boolean_expression.synth(circuit_type="bit")

super().__init__(oracle.num_qubits, name="Bit-flip Oracle")

self.compose(oracle, inplace=True, copy=False)

def evaluate_bitstring(self, bitstring: str) -> bool:
"""Evaluate the oracle on a bitstring.
This evaluation is done classically without any quantum circuit.
Args:
bitstring: The bitstring for which to evaluate. The input bitstring is expected to be
in little-endian order.
Returns:
True if the bitstring is a good state, False otherwise.
"""
return self.boolean_expression.simulate(bitstring[::-1])

@classmethod
def from_dimacs_file(cls, filename: str):
r"""Create a BitFlipOracle from the string in the DIMACS format.
It is possible to build a BitFlipOracle from a file in `DIMACS CNF format
<http://www.satcompetition.org/2009/format-benchmarks2009.html>`__,
which is the standard format for specifying SATisfiability (SAT) problem instances in
`Conjunctive Normal Form (CNF) <https://en.wikipedia.org/wiki/Conjunctive_normal_form>`__,
which is a conjunction of one or more clauses, where a clause is a disjunction of one
or more literals.
The following is an example of a CNF expressed in the DIMACS format:
.. code:: text
c DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.
p cnf 3 5
-1 -2 -3 0
1 -2 3 0
1 2 -3 0
1 -2 -3 0
-1 2 3 0
The first line, following the `c` character, is a comment. The second line specifies that
the CNF is over three boolean variables --- let us call them :math:`x_1, x_2, x_3`, and
contains five clauses. The five clauses, listed afterwards, are implicitly joined by the
logical `AND` operator, :math:`\land`, while the variables in each clause, represented by
their indices, are implicitly disjoined by the logical `OR` operator, :math:`lor`. The
:math:`-` symbol preceding a boolean variable index corresponds to the logical `NOT`
operator, :math:`lnot`. Character `0` (zero) marks the end of each clause. Essentially,
the code above corresponds to the following CNF:
:math:`(\lnot x_1 \lor \lnot x_2 \lor \lnot x_3)
\land (x_1 \lor \lnot x_2 \lor x_3)
\land (x_1 \lor x_2 \lor \lnot x_3)
\land (x_1 \lor \lnot x_2 \lor \lnot x_3)
\land (\lnot x_1 \lor x_2 \lor x_3)`.
Args:
filename: A file in DIMACS format.
Returns:
BitFlipOracle: A quantum circuit with a bit-flip oracle.
"""
expr = BooleanExpression.from_dimacs_file(filename)
return cls(expr)
Loading

0 comments on commit e403028

Please # to comment.