Repository: Qiskit/qiskit-optimization
Pull request #166 (2021-06-16 06:59:09)

Separate `from_ising` and `to_ising` out from `QuadraticProgram`
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add it in the CHANGELOG file under Unreleased section.
⚠️ If your pull request fixes an open issue, please link to the issue.

✅ I have added the tests to cover my changes.
✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
-->

### Summary

Separated `from_ising` and `to_ising` out from `QuadraticProgram` by adding them as translators.

### Details and comments




t-imamichi: Thanks to #170, I simplified this PR to remove `TYPE_CHECKING`.
t-imamichi: I notice that we need to raise an error in `from_ising` if any Pauli term has an imaginary coefficients. So, I added an error check and a unit test. In the previous implementation, coefficients were just converted to real values.

Patch:
diff --git a/.pylintdict b/.pylintdict
index 07e0368..95b983f 100644
--- a/.pylintdict
+++ b/.pylintdict
@@ -188,6 +188,7 @@ wavefunction
 wecker
 williamson
 xs
+ys
 zemlin
 zi
 zj
diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py
index e93b1fa..2d07e86 100644
--- a/qiskit_optimization/problems/quadratic_program.py
+++ b/qiskit_optimization/problems/quadratic_program.py
@@ -22,12 +22,10 @@ from typing import Dict, List, Optional, Tuple, Union, cast
 import numpy as np
 from docplex.mp.model import Model
 from docplex.mp.model_reader import ModelReader
-from numpy import ndarray, zeros
-from scipy.sparse import spmatrix
-
+from numpy import ndarray
 from qiskit.exceptions import MissingOptionalLibraryError
-from qiskit.opflow import I, ListOp, OperatorBase, PauliOp, PauliSumOp, SummedOp
-from qiskit.quantum_info import Pauli
+from qiskit.opflow import OperatorBase, PauliSumOp
+from scipy.sparse import spmatrix
 
 from ..exceptions import QiskitOptimizationError
 from ..infinity import INFINITY
@@ -1015,94 +1013,10 @@ class QuadraticProgram:
             QiskitOptimizationError: If a variable type is not binary.
             QiskitOptimizationError: If constraints exist in the problem.
         """
-        # if problem has variables that are not binary, raise an error
-        if self.get_num_vars() > self.get_num_binary_vars():
-            raise QiskitOptimizationError(
-                "The type of variable must be a binary variable. "
-                "Use a QuadraticProgramToQubo converter to convert "
-                "integer variables to binary variables. "
-                "If the problem contains continuous variables, "
-                "currently we can not apply VQE/QAOA directly. "
-                "you might want to use an ADMM optimizer "
-                "for the problem. "
-            )
-
-        # if constraints exist, raise an error
-        if self.linear_constraints or self.quadratic_constraints:
-            raise QiskitOptimizationError(
-                "An constraint exists. "
-                "The method supports only model with no constraints. "
-                "Use a QuadraticProgramToQubo converter. "
-                "It converts inequality constraints to equality "
-                "constraints, and then, it converters equality "
-                "constraints to penalty terms of the object function."
-            )
-
-        # initialize Hamiltonian.
-        num_nodes = self.get_num_vars()
-        pauli_list = []
-        offset = 0.0
-        zero = zeros(num_nodes, dtype=bool)
-
-        # set a sign corresponding to a maximized or minimized problem.
-        # sign == 1 is for minimized problem. sign == -1 is for maximized problem.
-        sense = self.objective.sense.value
-
-        # convert a constant part of the object function into Hamiltonian.
-        offset += self.objective.constant * sense
-
-        # convert linear parts of the object function into Hamiltonian.
-        for idx, coef in self.objective.linear.to_dict().items():
-            z_p = zeros(num_nodes, dtype=bool)
-            weight = coef * sense / 2
-            z_p[idx] = True
-
-            pauli_list.append([-weight, Pauli((z_p, zero))])
-            offset += weight
-
-        # convert quadratic parts of the object function into Hamiltonian.
-        # first merge coefficients (i, j) and (j, i)
-        coeffs = {}  # type: Dict
-        for (i, j), coeff in self.objective.quadratic.to_dict().items():
-            if j < i:  # type: ignore
-                coeffs[(j, i)] = coeffs.get((j, i), 0.0) + coeff
-            else:
-                coeffs[(i, j)] = coeffs.get((i, j), 0.0) + coeff
-
-        # create Pauli terms
-        for (i, j), coeff in coeffs.items():
-
-            weight = coeff * sense / 4
-
-            if i == j:
-                offset += weight
-            else:
-                z_p = zeros(num_nodes, dtype=bool)
-                z_p[i] = True
-                z_p[j] = True
-                pauli_list.append([weight, Pauli((z_p, zero))])
-
-            z_p = zeros(num_nodes, dtype=bool)
-            z_p[i] = True
-            pauli_list.append([-weight, Pauli((z_p, zero))])
-
-            z_p = zeros(num_nodes, dtype=bool)
-            z_p[j] = True
-            pauli_list.append([-weight, Pauli((z_p, zero))])
-
-            offset += weight
-
-        # Remove paulis whose coefficients are zeros.
-        qubit_op = sum(PauliOp(pauli, coeff=coeff) for coeff, pauli in pauli_list)
-
-        # qubit_op could be the integer 0, in this case return an identity operator of
-        # appropriate size
-        if isinstance(qubit_op, OperatorBase):
-            qubit_op = qubit_op.reduce()
-        else:
-            qubit_op = I ^ num_nodes
+        # pylint: disable=cyclic-import
+        from ..translators.ising import to_ising
 
-        return qubit_op, offset
+        return to_ising(self)
 
     def from_ising(
         self,
@@ -1129,102 +1043,12 @@ class QuadraticProgram:
             QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term
             NotImplementedError: If the input operator is a ListOp
         """
-        if isinstance(qubit_op, PauliSumOp):
-            qubit_op = qubit_op.to_pauli_op()
-
-        # No support for ListOp yet, this can be added in future
-        # pylint: disable=unidiomatic-typecheck
-        if type(qubit_op) == ListOp:
-            raise NotImplementedError(
-                "Conversion of a ListOp is not supported, convert each "
-                "operator in the ListOp separately."
-            )
-
-        # add binary variables
-        for i in range(qubit_op.num_qubits):
-            self.binary_var(name="x_{0}".format(i))
-
-        # Create a QUBO matrix
-        # The Qubo matrix is an upper triangular matrix.
-        # Diagonal elements in the QUBO matrix are for linear terms of the qubit operator.
-        # The other elements in the QUBO matrix are for quadratic terms of the qubit operator.
-        qubo_matrix = zeros((qubit_op.num_qubits, qubit_op.num_qubits))
-
-        if not isinstance(qubit_op, SummedOp):
-            pauli_list = [qubit_op.to_pauli_op()]
-        else:
-            pauli_list = qubit_op.to_pauli_op()
-
-        for pauli_op in pauli_list:
-            pauli_op = pauli_op.to_pauli_op()
-            pauli = pauli_op.primitive
-            coeff = pauli_op.coeff
-            # Count the number of Pauli Zs in a Pauli term
-            lst_z = pauli.z.tolist()
-            z_index = [i for i, z in enumerate(lst_z) if z is True]
-            num_z = len(z_index)
-
-            # Add its weight of the Pauli term to the corresponding element of QUBO matrix
-            if num_z == 1:
-                qubo_matrix[z_index[0], z_index[0]] = coeff.real
-            elif num_z == 2:
-                qubo_matrix[z_index[0], z_index[1]] = coeff.real
-            else:
-                raise QiskitOptimizationError(
-                    "There are more than 2 Pauli Zs in the Pauli term {}".format(pauli.z)
-                )
+        # pylint: disable=cyclic-import
+        from ..translators.ising import from_ising
 
-            # If there are Pauli Xs in the Pauli term, raise an error
-            lst_x = pauli.x.tolist()
-            x_index = [i for i, x in enumerate(lst_x) if x is True]
-            if len(x_index) > 0:
-                raise QiskitOptimizationError("Pauli Xs exist in the Pauli {}".format(pauli.x))
-
-        # Initialize dicts for linear terms and quadratic terms
-        linear_terms = {}
-        quadratic_terms = {}
-
-        # For quadratic pauli terms of operator
-        # x_i * x_ j = (1 - Z_i - Z_j + Z_i * Z_j)/4
-        for i, row in enumerate(qubo_matrix):
-            for j, weight in enumerate(row):
-                # Focus on the upper triangular matrix
-                if j <= i:
-                    continue
-                # Add a quadratic term to the object function of `QuadraticProgram`
-                # The coefficient of the quadratic term in `QuadraticProgram` is
-                # 4 * weight of the pauli
-                coef = weight * 4
-                quadratic_terms[i, j] = coef
-                # Sub the weight of the quadratic pauli term from the QUBO matrix
-                qubo_matrix[i, j] -= weight
-                # Sub the weight of the linear pauli term from the QUBO matrix
-                qubo_matrix[i, i] += weight
-                qubo_matrix[j, j] += weight
-                # Sub the weight from offset
-                offset -= weight
-
-        # After processing quadratic pauli terms, only linear paulis are left
-        # x_i = (1 - Z_i)/2
-        for i in range(qubit_op.num_qubits):
-            weight = qubo_matrix[i, i]
-            # Add a linear term to the object function of `QuadraticProgram`
-            # The coefficient of the linear term in `QuadraticProgram` is
-            # 2 * weight of the pauli
-            coef = weight * 2
-            if linear:
-                # If the linear option is True, add it into linear_terms
-                linear_terms[i] = -coef
-            else:
-                # Else, add it into quadratic_terms as a diagonal element.
-                quadratic_terms[i, i] = -coef
-            # Sub the weight of the linear pauli term from the QUBO matrix
-            qubo_matrix[i, i] -= weight
-            offset += weight
-
-        # Set the objective function
-        self.minimize(constant=offset, linear=linear_terms, quadratic=quadratic_terms)
-        offset -= offset
+        other = from_ising(qubit_op, offset, linear)
+        for attr, val in vars(other).items():
+            setattr(self, attr, val)
 
     def get_feasibility_info(
         self, x: Union[List[float], np.ndarray]
diff --git a/qiskit_optimization/translators/__init__.py b/qiskit_optimization/translators/__init__.py
index 4f8655f..3106810 100644
--- a/qiskit_optimization/translators/__init__.py
+++ b/qiskit_optimization/translators/__init__.py
@@ -29,9 +29,19 @@ Translators
    to_docplex_mp
    from_gurobipy
    to_gurobipy
+   from_ising
+   to_ising
 """
 
 from .docplex_mp import from_docplex_mp, to_docplex_mp
 from .gurobipy import from_gurobipy, to_gurobipy
+from .ising import from_ising, to_ising
 
-_all = ["from_docplex_mp", "to_docplex_mp", "from_gurobipy", "to_gurobipy"]
+_all = [
+    "from_docplex_mp",
+    "to_docplex_mp",
+    "from_gurobipy",
+    "to_gurobipy",
+    "from_ising",
+    "to_ising",
+]
diff --git a/qiskit_optimization/translators/ising.py b/qiskit_optimization/translators/ising.py
new file mode 100644
index 0000000..0c5ecf2
--- /dev/null
+++ b/qiskit_optimization/translators/ising.py
@@ -0,0 +1,222 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2019, 2021.
+#
+# 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.
+
+"""Translator between an Ising Hamiltonian and a quadratic program"""
+
+import math
+from typing import Tuple, Union
+
+import numpy as np
+
+from qiskit.opflow import I, ListOp, OperatorBase, PauliOp, PauliSumOp, SummedOp
+from qiskit.quantum_info import Pauli
+from qiskit_optimization.exceptions import QiskitOptimizationError
+from qiskit_optimization.problems.quadratic_program import QuadraticProgram
+
+
+def to_ising(quad_prog: QuadraticProgram) -> Tuple[OperatorBase, float]:
+    """Return the Ising Hamiltonian of this problem.
+
+    Variables are mapped to qubits in the same order, i.e.,
+    i-th variable is mapped to i-th qubit.
+    See https://github.com/Qiskit/qiskit-terra/issues/1148 for details.
+
+    Returns:
+        qubit_op: The qubit operator for the problem
+        offset: The constant value in the Ising Hamiltonian.
+
+    Raises:
+        QiskitOptimizationError: If an integer variable or a continuous variable exists
+            in the problem.
+        QiskitOptimizationError: If constraints exist in the problem.
+    """
+    # if problem has variables that are not binary, raise an error
+    if quad_prog.get_num_vars() > quad_prog.get_num_binary_vars():
+        raise QiskitOptimizationError(
+            "The type of all variables must be binary. "
+            "You can use `QuadraticProgramToQubo` converter "
+            "to convert integer variables to binary variables. "
+            "If the problem contains continuous variables, `to_ising` cannot handle it. "
+            "You might be able to solve it with `ADMMOptimizer`."
+        )
+
+    # if constraints exist, raise an error
+    if quad_prog.linear_constraints or quad_prog.quadratic_constraints:
+        raise QiskitOptimizationError(
+            "There must be no constraint in the problem. "
+            "You can use `QuadraticProgramToQubo` converter "
+            "to convert constraints to penalty terms of the objective function."
+        )
+
+    # initialize Hamiltonian.
+    num_nodes = quad_prog.get_num_vars()
+    pauli_list = []
+    offset = 0.0
+    zero = np.zeros(num_nodes, dtype=bool)
+
+    # set a sign corresponding to a maximized or minimized problem.
+    # sign == 1 is for minimized problem. sign == -1 is for maximized problem.
+    sense = quad_prog.objective.sense.value
+
+    # convert a constant part of the object function into Hamiltonian.
+    offset += quad_prog.objective.constant * sense
+
+    # convert linear parts of the object function into Hamiltonian.
+    for idx, coef in quad_prog.objective.linear.to_dict().items():
+        z_p = zero.copy()
+        weight = coef * sense / 2
+        z_p[idx] = True
+
+        pauli_list.append(PauliOp(Pauli((z_p, zero)), -weight))
+        offset += weight
+
+    # create Pauli terms
+    for (i, j), coeff in quad_prog.objective.quadratic.to_dict().items():
+        weight = coeff * sense / 4
+
+        if i == j:
+            offset += weight
+        else:
+            z_p = zero.copy()
+            z_p[i] = True
+            z_p[j] = True
+            pauli_list.append(PauliOp(Pauli((z_p, zero)), weight))
+
+        z_p = zero.copy()
+        z_p[i] = True
+        pauli_list.append(PauliOp(Pauli((z_p, zero)), -weight))
+
+        z_p = zero.copy()
+        z_p[j] = True
+        pauli_list.append(PauliOp(Pauli((z_p, zero)), -weight))
+
+        offset += weight
+
+    # Remove paulis whose coefficients are zeros.
+    qubit_op = sum(pauli_list)
+
+    # qubit_op could be the integer 0, in this case return an identity operator of
+    # appropriate size
+    if isinstance(qubit_op, OperatorBase):
+        qubit_op = qubit_op.reduce()
+    else:
+        qubit_op = I ^ num_nodes
+
+    return qubit_op, offset
+
+
+def from_ising(
+    qubit_op: Union[OperatorBase, PauliSumOp],
+    offset: float = 0.0,
+    linear: bool = False,
+) -> QuadraticProgram:
+    r"""Create a quadratic program from a qubit operator and a shift value.
+
+    Variables are mapped to qubits in the same order, i.e.,
+    i-th variable is mapped to i-th qubit.
+    See https://github.com/Qiskit/qiskit-terra/issues/1148 for details.
+
+    Args:
+        qubit_op: The qubit operator of the problem.
+        offset: The constant term in the Ising Hamiltonian.
+        linear: If linear is True, :math:`x^2` is treated as a linear term
+            since :math:`x^2 = x` for :math:`x \in \{0,1\}`.
+            Otherwise, :math:`x^2` is treat as a quadratic term.
+            The default value is False.
+
+    Returns:
+        The quadratic program corresponding to the qubit operator.
+
+    Raises:
+        QiskitOptimizationError: if there are Pauli Xs or Ys in any Pauli term
+        QiskitOptimizationError: if there are more than 2 Pauli Zs in any Pauli term
+        QiskitOptimizationError: if any Pauli term has an imaginary coefficient
+        NotImplementedError: If the input operator is a ListOp
+    """
+    if isinstance(qubit_op, PauliSumOp):
+        qubit_op = qubit_op.to_pauli_op()
+
+    # No support for ListOp yet, this can be added in future
+    # pylint: disable=unidiomatic-typecheck
+    if type(qubit_op) == ListOp:
+        raise NotImplementedError(
+            "Conversion of a ListOp is not supported, convert each "
+            "operator in the ListOp separately."
+        )
+
+    quad_prog = QuadraticProgram()
+    quad_prog.binary_var_list(qubit_op.num_qubits)
+
+    if not isinstance(qubit_op, SummedOp):
+        pauli_list = [qubit_op.to_pauli_op()]
+    else:
+        pauli_list = qubit_op.to_pauli_op()
+
+    # prepare a matrix of coefficients of Pauli terms
+    # `pauli_coeffs_diag` is the diagonal part
+    # `pauli_coeffs_triu` is the upper triangular part
+    pauli_coeffs_diag = [0.0] * qubit_op.num_qubits
+    pauli_coeffs_triu = {}
+
+    for pauli_op in pauli_list:
+        pauli_op = pauli_op.to_pauli_op()
+        pauli = pauli_op.primitive
+        coeff = pauli_op.coeff
+
+        if not math.isclose(coeff.imag, 0.0, abs_tol=1e-10):
+            raise QiskitOptimizationError(f"Imaginary coefficient exists: {pauli_op}")
+
+        if np.any(pauli.x):
+            raise QiskitOptimizationError(f"Pauli X or Y exists in the Pauli term: {pauli}")
+
+        # indices of Pauli Zs in the Pauli term
+        z_index = np.where(pauli.z)[0]
+        num_z = len(z_index)
+
+        if num_z == 1:
+            pauli_coeffs_diag[z_index[0]] = coeff.real
+        elif num_z == 2:
+            pauli_coeffs_triu[z_index[0], z_index[1]] = coeff.real
+        else:
+            raise QiskitOptimizationError(
+                f"There are more than 2 Pauli Zs in the Pauli term: {pauli}"
+            )
+
+    linear_terms = {}
+    quadratic_terms = {}
+
+    # For quadratic pauli terms of operator
+    # x_i * x_j = (1 - Z_i - Z_j + Z_i * Z_j)/4
+    for (i, j), weight in pauli_coeffs_triu.items():
+        # Add a quadratic term to the object function of `QuadraticProgram`
+        # The coefficient of the quadratic term in `QuadraticProgram` is
+        # 4 * weight of the pauli
+        quadratic_terms[i, j] = 4 * weight
+        pauli_coeffs_diag[i] += weight
+        pauli_coeffs_diag[j] += weight
+        offset -= weight
+
+    # After processing quadratic pauli terms, only linear paulis are left
+    # x_i = (1 - Z_i)/2
+    for i, weight in enumerate(pauli_coeffs_diag):
+        # Add a linear term to the object function of `QuadraticProgram`
+        # The coefficient of the linear term in `QuadraticProgram` is
+        # 2 * weight of the pauli
+        if linear:
+            linear_terms[i] = -2 * weight
+        else:
+            quadratic_terms[i, i] = -2 * weight
+        offset += weight
+
+    quad_prog.minimize(constant=offset, linear=linear_terms, quadratic=quadratic_terms)
+
+    return quad_prog
diff --git a/releasenotes/notes/add-ising-translator-b93e22adda321e56.yaml b/releasenotes/notes/add-ising-translator-b93e22adda321e56.yaml
new file mode 100644
index 0000000..01e5031
--- /dev/null
+++ b/releasenotes/notes/add-ising-translator-b93e22adda321e56.yaml
@@ -0,0 +1,5 @@
+features:
+  - |
+    Adds translators between Ising Hamiltonian and ``QuadraticProgram``,
+    :meth:`~qiskit_optimization.translators.from_ising` and
+    :meth:`~qiskit_optimization.translators.to_ising`.


Test patch:
diff --git a/test/translators/test_ising.py b/test/translators/test_ising.py
new file mode 100644
index 0000000..7a37a01
--- /dev/null
+++ b/test/translators/test_ising.py
@@ -0,0 +1,152 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2021.
+#
+# 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.
+
+"""Test from_ising and to_ising"""
+
+from test.optimization_test_case import QiskitOptimizationTestCase
+
+import numpy as np
+
+from qiskit.opflow import PauliSumOp
+from qiskit_optimization.exceptions import QiskitOptimizationError
+from qiskit_optimization.problems import QuadraticProgram
+from qiskit_optimization.translators import from_ising, to_ising
+
+
+class TestIsingTranslator(QiskitOptimizationTestCase):
+    """Test from_ising and to_ising"""
+
+    def test_to_ising(self):
+        """test to_ising"""
+
+        with self.subTest("minimize"):
+            # minimize: x + x * y
+            # subject to: x, y \in {0, 1}
+            q_p = QuadraticProgram("test")
+            q_p.binary_var(name="x")
+            q_p.binary_var(name="y")
+            q_p.minimize(linear={"x": 1}, quadratic={("x", "y"): 1})
+            op, offset = to_ising(q_p)
+            op_ref = PauliSumOp.from_list([("ZI", -0.25), ("IZ", -0.75), ("ZZ", 0.25)])
+            np.testing.assert_array_almost_equal(op.to_matrix(), op_ref.to_matrix())
+            self.assertAlmostEqual(offset, 0.75)
+
+        with self.subTest("maximize"):
+            # maximize: x + x * y
+            # subject to: x, y \in {0, 1}
+            q_p = QuadraticProgram("test")
+            q_p.binary_var(name="x")
+            q_p.binary_var(name="y")
+            q_p.maximize(linear={"x": 1}, quadratic={("x", "y"): 1})
+            op, offset = to_ising(q_p)
+            op_ref = PauliSumOp.from_list([("ZI", 0.25), ("IZ", 0.75), ("ZZ", -0.25)])
+            np.testing.assert_array_almost_equal(op.to_matrix(), op_ref.to_matrix())
+            self.assertAlmostEqual(offset, -0.75)
+
+    def test_to_ising2(self):
+        """test to_ising 2"""
+
+        with self.subTest("minimize"):
+            # minimize: 1 - 2 * x1 - 2 * x2 + 4 * x1 * x2
+            # subject to: x, y \in {0, 1}
+            q_p = QuadraticProgram("test")
+            q_p.binary_var(name="x")
+            q_p.binary_var(name="y")
+            q_p.minimize(constant=1, linear={"x": -2, "y": -2}, quadratic={("x", "y"): 4})
+            op, offset = to_ising(q_p)
+            op_ref = PauliSumOp.from_list([("ZZ", 1.0)])
+            np.testing.assert_array_almost_equal(op.to_matrix(), op_ref.to_matrix())
+            self.assertAlmostEqual(offset, 0.0)
+
+        with self.subTest("maximize"):
+            # maximize: 1 - 2 * x1 - 2 * x2 + 4 * x1 * x2
+            # subject to: x, y \in {0, 1}
+            q_p = QuadraticProgram("test")
+            q_p.binary_var(name="x")
+            q_p.binary_var(name="y")
+            q_p.maximize(constant=1, linear={"x": -2, "y": -2}, quadratic={("x", "y"): 4})
+            op, offset = to_ising(q_p)
+            op_ref = PauliSumOp.from_list([("ZZ", -1.0)])
+            np.testing.assert_array_almost_equal(op.to_matrix(), op_ref.to_matrix())
+            self.assertAlmostEqual(offset, 0.0)
+
+    def test_from_ising(self):
+        """test to_from_ising"""
+        # minimize: x + x * y
+        # subject to: x, y \in {0, 1}
+        op = PauliSumOp.from_list([("ZI", -0.25), ("IZ", -0.75), ("ZZ", 0.25)])
+        with self.subTest("linear: True"):
+            q_p = from_ising(op, 0.75, linear=True)
+            self.assertEqual(q_p.get_num_vars(), op.num_qubits)
+            self.assertEqual(q_p.get_num_linear_constraints(), 0)
+            self.assertEqual(q_p.get_num_quadratic_constraints(), 0)
+            self.assertAlmostEqual(q_p.objective.constant, 0)
+            np.testing.assert_array_almost_equal(q_p.objective.linear.to_array(), [1, 0])
+            np.testing.assert_array_almost_equal(
+                q_p.objective.quadratic.to_array(), [[0, 1], [0, 0]]
+            )
+
+        with self.subTest("linear: False"):
+            q_p = from_ising(op, 0.75, linear=False)
+            self.assertEqual(q_p.get_num_vars(), op.num_qubits)
+            self.assertEqual(q_p.get_num_linear_constraints(), 0)
+            self.assertEqual(q_p.get_num_quadratic_constraints(), 0)
+            self.assertAlmostEqual(q_p.objective.constant, 0)
+            np.testing.assert_array_almost_equal(q_p.objective.linear.to_array(), [0, 0])
+            np.testing.assert_array_almost_equal(
+                q_p.objective.quadratic.to_array(), [[1, 1], [0, 0]]
+            )
+
+    def test_from_ising2(self):
+        """test to_from_ising 2"""
+        # minimize: 1 - 2 * x1 - 2 * x2 + 4 * x1 * x2
+        # subject to: x, y \in {0, 1}
+        op = PauliSumOp.from_list([("ZZ", 1)])
+        with self.subTest("linear: True"):
+            q_p = from_ising(op, 0, linear=True)
+            self.assertEqual(q_p.get_num_vars(), op.num_qubits)
+            self.assertEqual(q_p.get_num_linear_constraints(), 0)
+            self.assertEqual(q_p.get_num_quadratic_constraints(), 0)
+            self.assertAlmostEqual(q_p.objective.constant, 1)
+            np.testing.assert_array_almost_equal(q_p.objective.linear.to_array(), [-2, -2])
+            np.testing.assert_array_almost_equal(
+                q_p.objective.quadratic.to_array(), [[0, 4], [0, 0]]
+            )
+
+        with self.subTest("linear: False"):
+            q_p = from_ising(op, 0, linear=False)
+            self.assertEqual(q_p.get_num_vars(), op.num_qubits)
+            self.assertEqual(q_p.get_num_linear_constraints(), 0)
+            self.assertEqual(q_p.get_num_quadratic_constraints(), 0)
+            self.assertAlmostEqual(q_p.objective.constant, 1)
+            np.testing.assert_array_almost_equal(q_p.objective.linear.to_array(), [0, 0])
+            np.testing.assert_array_almost_equal(
+                q_p.objective.quadratic.to_array(), [[-2, 4], [0, -2]]
+            )
+
+    def test_from_ising_pauli_with_invalid_paulis(self):
+        """test to_from_ising with invalid Pauli terms"""
+        with self.assertRaises(QiskitOptimizationError):
+            op = PauliSumOp.from_list([("IX", 1)])
+            _ = from_ising(op, 0)
+
+        with self.assertRaises(QiskitOptimizationError):
+            op = PauliSumOp.from_list([("IY", 1)])
+            _ = from_ising(op, 0)
+
+        with self.assertRaises(QiskitOptimizationError):
+            op = PauliSumOp.from_list([("ZZZ", 1)])
+            _ = from_ising(op, 0)
+
+        with self.assertRaises(QiskitOptimizationError):
+            op = PauliSumOp.from_list([("IZ", 1j)])
+            _ = from_ising(op, 0)