Repository: Qiskit/qiskit — pull request #14636 (python)
Title: Fixes for dimacs-related boolean expression code

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nThis PR fixes a bug in the code for creating phase and bit-flip oracles from DIMACS files.\r\n\r\nFixes #14634\r\n\r\n\r\n### Details and comments\r\nCurrently, oracles are generated from DIMACS files by using `BooleanExpression.from_dimacs` to generate an expression and pass it to the init of the oracle. However, the init currently expects a string representing a boolean expression, not the expression itself.\r\n\r\nThis PR changes the behavior such that the oracles can be initialized from a `BooleanExpression` object, and slightly improves DIMACS parsing.\r\n\r\n

Resolved issues:
title: `PhaseOracle` fails to instantiate from a Dimacs file
body: ### Environment\n\n- **Qiskit version**: 2.0.2 (can't reproduce with 1.4.1)\n- **Python version**: 3.13.3\n- **Operating system**: Arch Linux\n\n\n### What is happening?\n\nTrying to instantiate a `PhaseOracle` using a Dimacs file via the `PhaseOracle.from_dimacs_file` method results in a `TypeError`.\n\n### How can we reproduce the issue?\n\n```python\nimport os\nimport tempfile\nfrom qiskit.circuit.library.phase_oracle import PhaseOracle\n\ninput_3sat_instance = """\nc DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\np cnf 3 5\n-1 -2 -3 0\n1 -2 3 0\n1 2 -3 0\n1 -2 -3 0\n-1 2 3 0\n"""\n\nwith open("tmpfile", 'w') as f:\n    f.write(input_3sat_instance)\n\noracle = PhaseOracle.from_dimacs_file("tmpfile")\n```\n\n### What should happen?\n\nThe `PhaseOracle` should be instantiated without error.\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/qiskit/circuit/library/bit_flip_oracle.py b/qiskit/circuit/library/bit_flip_oracle.py\nindex fbd45c11a4c2..c342a6e27ed1 100644\n--- a/qiskit/circuit/library/bit_flip_oracle.py\n+++ b/qiskit/circuit/library/bit_flip_oracle.py\n@@ -52,23 +52,27 @@ class BitFlipOracleGate(Gate):\n \n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n         label: str | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n             label: A label for the gate to display in visualizations. Per default, the label is\n                 set to display the textual represntation of the boolean expression (truncated if needed)\n         """\n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n-\n         if label is None:\n-            short_expr_for_name = (expression[:15] + "...") if len(expression) > 15 else expression\n-            label = short_expr_for_name\n+            if isinstance(expression, str):\n+                label = (expression[:15] + "...") if len(expression) > 15 else expression\n+            else:\n+                label = "Boolean Expression"\n+\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n \n         super().__init__(\n             name="Bit-flip Oracle",\ndiff --git a/qiskit/circuit/library/phase_oracle.py b/qiskit/circuit/library/phase_oracle.py\nindex 4b687976b296..96ccc58a798e 100644\n--- a/qiskit/circuit/library/phase_oracle.py\n+++ b/qiskit/circuit/library/phase_oracle.py\n@@ -57,17 +57,19 @@ class PhaseOracle(QuantumCircuit):\n     )\n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n         """\n \n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n         oracle = self.boolean_expression.synth(circuit_type="phase")\n \n         super().__init__(oracle.num_qubits, name="Phase Oracle")\n@@ -168,23 +170,27 @@ class PhaseOracleGate(Gate):\n \n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n         label: str | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n             label: A label for the gate to display in visualizations. Per default, the label is\n                 set to display the textual represntation of the boolean expression (truncated if needed)\n         """\n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n-\n         if label is None:\n-            short_expr_for_name = (expression[:15] + "...") if len(expression) > 15 else expression\n-            label = short_expr_for_name\n+            if isinstance(expression, str):\n+                label = (expression[:15] + "...") if len(expression) > 15 else expression\n+            else:\n+                label = "Boolean Expression"\n+\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n \n         super().__init__(\n             name="Phase Oracle",\ndiff --git a/qiskit/synthesis/boolean/boolean_expression.py b/qiskit/synthesis/boolean/boolean_expression.py\nindex 5122dbf9637d..1aaa348d16e8 100644\n--- a/qiskit/synthesis/boolean/boolean_expression.py\n+++ b/qiskit/synthesis/boolean/boolean_expression.py\n@@ -191,8 +191,9 @@ def from_dimacs(dimacs: str):\n         """\n         header_regex = re.compile(r"p\\s+cnf\\s+(\\d+)\\s+(\\d+)")\n         clause_regex = re.compile(r"(-?\\d+)")\n+        lines = [line.strip() for line in dimacs.split("\\n")]\n         lines = [\n-            line for line in dimacs.split("\\n") if not line.startswith("c") and line != ""\n+            line for line in lines if not line.startswith("c") and line != ""\n         ]  # DIMACS comment line start with c\n         header_match = header_regex.match(lines[0])\n         if not header_match:\ndiff --git a/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml b/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml\nnew file mode 100644\nindex 000000000000..98a708d5f0c1\n--- /dev/null\n+++ b/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml\n@@ -0,0 +1,5 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in :class:`.PhaseOracle`, :class:`.PhaseOracleGate` and :class:`.BitFlipOracleGate`\n+    where trying to load from dimacs file raised a TypeError.\n\\ No newline at end of file\n

Test patch:
diff --git a/test/python/circuit/library/test_phase_and_bitflip_oracles.py b/test/python/circuit/library/test_phase_and_bitflip_oracles.py\nindex ca67c6d232f3..71e209124c8d 100644\n--- a/test/python/circuit/library/test_phase_and_bitflip_oracles.py\n+++ b/test/python/circuit/library/test_phase_and_bitflip_oracles.py\n@@ -13,6 +13,7 @@\n """Test the phase and bit-flip oracle circuits."""\n \n import unittest\n+import tempfile\n from ddt import ddt, data, unpack\n from numpy import sqrt, isclose\n \n@@ -109,6 +110,28 @@ def test_variable_order(self, expression, var_order, good_states):\n                 self.assertListEqual(expected_valid, result_valid)\n                 self.assertListEqual(expected_invalid, result_invalid)\n \n+    def test_from_dimacs_file(self):\n+        """Initializing from DIMACS file"""\n+        input_3sat_instance = """\n+        c DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\n+        p cnf 3 5\n+        -1 -2 -3 0\n+        1 -2 3 0\n+        1 2 -3 0\n+        1 -2 -3 0\n+        -1 2 3 0\n+        """\n+        filename = tempfile.mkstemp(suffix=".dimacs")[1]\n+        with open(filename, "w") as file:\n+            file.write(input_3sat_instance)\n+        for use_gate in [True, False]:\n+            if use_gate:\n+                oracle = PhaseOracleGate.from_dimacs_file(filename)\n+            else:\n+                with self.assertWarns(DeprecationWarning):\n+                    oracle = PhaseOracle.from_dimacs_file(filename)\n+            self.assertEqual(oracle.num_qubits, 3)\n+\n \n @ddt\n class TestBitFlipOracleGate(QiskitTestCase):\n@@ -180,6 +203,23 @@ def test_variable_order(self, expression, var_order, truth_table):\n         self.assertListEqual(expected_valid, result_valid)\n         self.assertListEqual(expected_invalid, result_invalid)\n \n+    def test_from_dimacs_file(self):\n+        """Initializing from DIMACS file"""\n+        input_3sat_instance = """\n+        c DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\n+        p cnf 3 5\n+        -1 -2 -3 0\n+        1 -2 3 0\n+        1 2 -3 0\n+        1 -2 -3 0\n+        -1 2 3 0\n+        """\n+        filename = tempfile.mkstemp(suffix=".dimacs")[1]\n+        with open(filename, "w") as file:\n+            file.write(input_3sat_instance)\n+        oracle = BitFlipOracleGate.from_dimacs_file(filename)\n+        self.assertEqual(oracle.num_qubits, 4)\n+\n \n if __name__ == "__main__":\n     unittest.main()\ndiff --git a/test/python/synthesis/test_boolean.py b/test/python/synthesis/test_boolean.py\nindex 511e95e9b0e6..f84ad8f7da10 100644\n--- a/test/python/synthesis/test_boolean.py\n+++ b/test/python/synthesis/test_boolean.py\n@@ -169,10 +169,6 @@ def test_quinn(self):\n     def test_bad_formatting(self):\n         """Tests DIMACS parsing on edge cases"""\n         # pylint: disable=trailing-whitespace\n-        dimacs = """ \n-p cnf 10 5"""  # first line is not p cnf nor empty nor comment (it has whitespace)\n-        with self.assertRaisesRegex(ValueError, "First line must start with 'p cnf'"):\n-            exp = BooleanExpression.from_dimacs(dimacs)\n         dimacs = """p cnf 2 1\n          \n         1 2 0"""  # has empty line with whitespace - should ignore it\n