Repository: Qiskit/qiskit-optimization
Pull request #169 (2021-06-16 13:57:10)

Add lower bound and upper bound of `LinearExpression` and `QuadraticExpression`
<!--
⚠️ 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

Add `LinearExpression.bounds()` and `QuadraticExpression.bounds()` to obtain a tuple of the lower bound and the upper bound.
They can be used for `LinearEqualityToPenalty`, `LinearInequalityToPenalty`, and `from_docplex_mp` (indicator constraint) and this PR may simplifies them.

This also simplifies `LinearEqualityToPenalty` and `InequalityToEquality` using the bounds.
This can be used for #157 too. But it does not work for #163.

TODO:
- [x] Discussion whether we need this feature or not.

### Details and comments




t-imamichi: I notice that `from_docplex_mp` (indicator constraint) cannot use this feature to compute LB and UB. `LinearEqualityToPenalty` does not always require this because it handles a easier case (all variables are converted into binary).
This PR is necessary only for `LinearInequalityToPenalty` #157. So, we don't need this PR and #157 can implement the same feature as part of their converter.
t-imamichi: I found this can be used in `InequalityToEquality` as well as `LinearInequalityToPenalty` and `EqualityToPenalty`.
So, I want to revise this PR to simplify the converters.
https://github.com/Qiskit/qiskit-optimization/blob/cfef1fee1185b82222a11fce846885e655c8fce5/qiskit_optimization/converters/inequality_to_equality.py#L275
https://github.com/Qiskit/qiskit-optimization/blob/cfef1fee1185b82222a11fce846885e655c8fce5/qiskit_optimization/converters/inequality_to_equality.py#L304
https://github.com/Qiskit/qiskit-optimization/blob/main/qiskit_optimization/converters/linear_equality_to_penalty.py#L159-L162

Patch:
diff --git a/qiskit_optimization/converters/inequality_to_equality.py b/qiskit_optimization/converters/inequality_to_equality.py
index ecd7ec3..cae6ac6 100644
--- a/qiskit_optimization/converters/inequality_to_equality.py
+++ b/qiskit_optimization/converters/inequality_to_equality.py
@@ -13,16 +13,18 @@
 
 import copy
 import math
-from typing import List, Optional, Union
+from typing import List, Optional, Union, cast
 
 import numpy as np
 
-from .quadratic_program_converter import QuadraticProgramConverter
 from ..exceptions import QiskitOptimizationError
 from ..problems.constraint import Constraint
+from ..problems.linear_constraint import LinearConstraint
+from ..problems.quadratic_constraint import QuadraticConstraint
 from ..problems.quadratic_objective import QuadraticObjective
 from ..problems.quadratic_program import QuadraticProgram
 from ..problems.variable import Variable
+from .quadratic_program_converter import QuadraticProgramConverter
 
 
 class InequalityToEquality(QuadraticProgramConverter):
@@ -72,6 +74,8 @@ class InequalityToEquality(QuadraticProgramConverter):
 
         # set a converting mode
         mode = self._mode
+        if mode not in ["integer", "continuous", "auto"]:
+            raise QiskitOptimizationError(f"Unsupported mode is selected: {mode}")
 
         # Copy variables
         for x in self._src.variables:
@@ -84,7 +88,7 @@ class InequalityToEquality(QuadraticProgramConverter):
                     name=x.name, lowerbound=x.lowerbound, upperbound=x.upperbound
                 )
             else:
-                raise QiskitOptimizationError("Unsupported variable type {}".format(x.vartype))
+                raise QiskitOptimizationError(f"Unsupported variable type {x.vartype}")
 
         # Copy the objective function
         constant = self._src.objective.constant
@@ -96,279 +100,142 @@ class InequalityToEquality(QuadraticProgramConverter):
             self._dst.maximize(constant, linear, quadratic)
 
         # For linear constraints
-        for l_constraint in self._src.linear_constraints:
-            linear = l_constraint.linear.to_dict(use_name=True)
-            if l_constraint.sense == Constraint.Sense.EQ:
+        for lin_const in self._src.linear_constraints:
+            if lin_const.sense == Constraint.Sense.EQ:
                 self._dst.linear_constraint(
-                    linear, l_constraint.sense, l_constraint.rhs, l_constraint.name
+                    lin_const.linear.coefficients, lin_const.sense, lin_const.rhs, lin_const.name
                 )
-            elif (
-                l_constraint.sense == Constraint.Sense.LE
-                or l_constraint.sense == Constraint.Sense.GE
-            ):
-                if mode == "integer":
-                    self._add_integer_slack_var_linear_constraint(
-                        linear, l_constraint.sense, l_constraint.rhs, l_constraint.name
-                    )
-                elif mode == "continuous":
-                    self._add_continuous_slack_var_linear_constraint(
-                        linear, l_constraint.sense, l_constraint.rhs, l_constraint.name
-                    )
-                elif mode == "auto":
-                    self._add_auto_slack_var_linear_constraint(
-                        linear, l_constraint.sense, l_constraint.rhs, l_constraint.name
-                    )
-                else:
-                    raise QiskitOptimizationError("Unsupported mode is selected: {}".format(mode))
+            elif lin_const.sense in [Constraint.Sense.LE, Constraint.Sense.GE]:
+                self._add_slack_var_linear_constraint(lin_const)
             else:
                 raise QiskitOptimizationError(
-                    "Type of sense in {} is not supported".format(l_constraint.name)
+                    f"Internal error: type of sense in {lin_const.name} is not supported: "
+                    f"{lin_const.sense}"
                 )
 
         # For quadratic constraints
-        for q_constraint in self._src.quadratic_constraints:
-            linear = q_constraint.linear.to_dict(use_name=True)
-            quadratic = q_constraint.quadratic.to_dict(use_name=True)
-            if q_constraint.sense == Constraint.Sense.EQ:
+        for quad_const in self._src.quadratic_constraints:
+            if quad_const.sense == Constraint.Sense.EQ:
                 self._dst.quadratic_constraint(
-                    linear,
-                    quadratic,
-                    q_constraint.sense,
-                    q_constraint.rhs,
-                    q_constraint.name,
+                    quad_const.linear.coefficients,
+                    quad_const.quadratic.coefficients,
+                    quad_const.sense,
+                    quad_const.rhs,
+                    quad_const.name,
                 )
-            elif (
-                q_constraint.sense == Constraint.Sense.LE
-                or q_constraint.sense == Constraint.Sense.GE
-            ):
-                if mode == "integer":
-                    self._add_integer_slack_var_quadratic_constraint(
-                        linear,
-                        quadratic,
-                        q_constraint.sense,
-                        q_constraint.rhs,
-                        q_constraint.name,
-                    )
-                elif mode == "continuous":
-                    self._add_continuous_slack_var_quadratic_constraint(
-                        linear,
-                        quadratic,
-                        q_constraint.sense,
-                        q_constraint.rhs,
-                        q_constraint.name,
-                    )
-                elif mode == "auto":
-                    self._add_auto_slack_var_quadratic_constraint(
-                        linear,
-                        quadratic,
-                        q_constraint.sense,
-                        q_constraint.rhs,
-                        q_constraint.name,
-                    )
-                else:
-                    raise QiskitOptimizationError("Unsupported mode is selected: {}".format(mode))
+            elif quad_const.sense in [Constraint.Sense.LE, Constraint.Sense.GE]:
+                self._add_slack_var_quadratic_constraint(quad_const)
             else:
                 raise QiskitOptimizationError(
-                    "Type of sense in {} is not supported".format(q_constraint.name)
+                    f"Internal error: type of sense in {quad_const.name} is not supported: "
+                    f"{quad_const.sense}"
                 )
 
         return self._dst
 
-    def _add_integer_slack_var_linear_constraint(self, linear, sense, rhs, name):
-        # If a coefficient that is not integer exist, raise error
-        if self._contains_any_float_value(linear.values()):
-            raise QiskitOptimizationError(
-                '"{0}" contains float coefficients. '
-                'We can not use an integer slack variable for "{0}"'.format(name)
-            )
-
-        # If rhs is float number, round up/down to the nearest integer.
-        new_rhs = rhs
-        if sense == Constraint.Sense.LE:
-            new_rhs = math.floor(rhs)
-        if sense == Constraint.Sense.GE:
-            new_rhs = math.ceil(rhs)
-
-        # Add a new integer variable.
-        slack_name = name + self._delimiter + "int_slack"
-
-        lhs_lb, lhs_ub = self._calc_linear_bounds(linear)
-
-        var_added = False
+    def _add_slack_var_linear_constraint(self, constraint: LinearConstraint):
+        linear = constraint.linear
+        sense = constraint.sense
+        name = constraint.name
 
+        any_float = self._any_float(linear.to_array())
+        mode = self._mode
+        if mode == "integer":
+            if any_float:
+                raise QiskitOptimizationError(
+                    f'"{name}" contains float coefficients. '
+                    'We can not use an integer slack variable for "{name}"'
+                )
+        elif mode == "auto":
+            mode = "continuous" if any_float else "integer"
+
+        new_rhs = constraint.rhs
+        if mode == "integer":
+            # If rhs is float number, round up/down to the nearest integer.
+            if sense == Constraint.Sense.LE:
+                new_rhs = math.floor(new_rhs)
+            if sense == Constraint.Sense.GE:
+                new_rhs = math.ceil(new_rhs)
+
+        lin_bounds = linear.bounds
+        lhs_lb = lin_bounds.lowerbound
+        lhs_ub = lin_bounds.upperbound
+
+        var_ub = 0.0
+        sign = 0
         if sense == Constraint.Sense.LE:
             var_ub = new_rhs - lhs_lb
             if var_ub > 0:
                 sign = 1
-                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
         elif sense == Constraint.Sense.GE:
             var_ub = lhs_ub - new_rhs
             if var_ub > 0:
                 sign = -1
-                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
-        else:
-            raise QiskitOptimizationError("The type of Sense in {} is not supported".format(name))
-
-        # Add a new equality constraint.
-        new_linear = copy.deepcopy(linear)
-        if var_added:
-            new_linear[slack_name] = sign
-        self._dst.linear_constraint(new_linear, "==", new_rhs, name)
-
-    def _add_continuous_slack_var_linear_constraint(self, linear, sense, rhs, name):
-        slack_name = name + self._delimiter + "continuous_slack"
-
-        lhs_lb, lhs_ub = self._calc_linear_bounds(linear)
 
-        var_added = False
-        if sense == Constraint.Sense.LE:
-            var_ub = rhs - lhs_lb
-            if var_ub > 0:
-                sign = 1
-                self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
-        elif sense == Constraint.Sense.GE:
-            var_ub = lhs_ub - rhs
-            if var_ub > 0:
-                sign = -1
+        new_linear = linear.to_dict(use_name=True)
+        if var_ub > 0:
+            # Add a slack variable.
+            mode_name = {"integer": "int", "continuous": "continuous"}
+            slack_name = f"{name}{self._delimiter}{mode_name[mode]}_slack"
+            if mode == "integer":
+                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
+            elif mode == "continuous":
                 self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
-        else:
-            raise QiskitOptimizationError("The type of Sense in {} is not supported".format(name))
-
-        # Add a new equality constraint.
-        new_linear = copy.deepcopy(linear)
-        if var_added:
             new_linear[slack_name] = sign
-        self._dst.linear_constraint(new_linear, "==", rhs, name)
-
-    def _add_auto_slack_var_linear_constraint(self, linear, sense, rhs, name):
-        # If a coefficient that is not integer exist, use a continuous slack variable
-        if self._contains_any_float_value(list(linear.values())):
-            self._add_continuous_slack_var_linear_constraint(linear, sense, rhs, name)
-        # Else use an integer slack variable
-        else:
-            self._add_integer_slack_var_linear_constraint(linear, sense, rhs, name)
-
-    def _add_integer_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name):
-        # If a coefficient that is not integer exist, raise an error
-        if self._contains_any_float_value(list(linear.values())) or self._contains_any_float_value(
-            list(quadratic.values())
-        ):
-            raise QiskitOptimizationError(
-                '"{0}" contains float coefficients. '
-                'We can not use an integer slack variable for "{0}"'.format(name)
-            )
-
-        # If rhs is float number, round up/down to the nearest integer.
-        new_rhs = rhs
-        if sense == Constraint.Sense.LE:
-            new_rhs = math.floor(rhs)
-        if sense == Constraint.Sense.GE:
-            new_rhs = math.ceil(rhs)
-
-        # Add a new integer variable.
-        slack_name = name + self._delimiter + "int_slack"
-
-        lhs_lb, lhs_ub = self._calc_quadratic_bounds(linear, quadratic)
+        self._dst.linear_constraint(new_linear, "==", new_rhs, name)
 
-        var_added = False
+    def _add_slack_var_quadratic_constraint(self, constraint: QuadraticConstraint):
+        quadratic = constraint.quadratic
+        linear = constraint.linear
+        sense = constraint.sense
+        name = constraint.name
 
+        any_float = self._any_float(linear.to_array()) or self._any_float(quadratic.to_array())
+        mode = self._mode
+        if mode == "integer":
+            if any_float:
+                raise QiskitOptimizationError(
+                    f'"{name}" contains float coefficients. '
+                    'We can not use an integer slack variable for "{name}"'
+                )
+        elif mode == "auto":
+            mode = "continuous" if any_float else "integer"
+
+        new_rhs = constraint.rhs
+        if mode == "integer":
+            # If rhs is float number, round up/down to the nearest integer.
+            if sense == Constraint.Sense.LE:
+                new_rhs = math.floor(new_rhs)
+            if sense == Constraint.Sense.GE:
+                new_rhs = math.ceil(new_rhs)
+
+        lin_bounds = linear.bounds
+        quad_bounds = quadratic.bounds
+        lhs_lb = lin_bounds.lowerbound + quad_bounds.lowerbound
+        lhs_ub = lin_bounds.upperbound + quad_bounds.upperbound
+
+        var_ub = 0.0
+        sign = 0
         if sense == Constraint.Sense.LE:
             var_ub = new_rhs - lhs_lb
             if var_ub > 0:
                 sign = 1
-                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
         elif sense == Constraint.Sense.GE:
             var_ub = lhs_ub - new_rhs
             if var_ub > 0:
                 sign = -1
-                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
-        else:
-            raise QiskitOptimizationError("The type of Sense in {} is not supported".format(name))
-
-        # Add a new equality constraint.
-        new_linear = copy.deepcopy(linear)
-        if var_added:
-            new_linear[slack_name] = sign
-        self._dst.quadratic_constraint(new_linear, quadratic, "==", new_rhs, name)
 
-    def _add_continuous_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name):
-        # Add a new continuous variable.
-        slack_name = name + self._delimiter + "continuous_slack"
-
-        lhs_lb, lhs_ub = self._calc_quadratic_bounds(linear, quadratic)
-
-        var_added = False
-
-        if sense == Constraint.Sense.LE:
-            var_ub = rhs - lhs_lb
-            if var_ub > 0:
-                sign = 1
+        new_linear = linear.to_dict(use_name=True)
+        if var_ub > 0:
+            # Add a slack variable.
+            mode_name = {"integer": "int", "continuous": "continuous"}
+            slack_name = f"{name}{self._delimiter}{mode_name[mode]}_slack"
+            if mode == "integer":
+                self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=var_ub)
+            elif mode == "continuous":
                 self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=var_ub)
-                var_added = True
-        elif sense == Constraint.Sense.GE:
-            var_ub = lhs_ub - rhs
-            if var_ub > 0:
-                sign = -1
-                self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=lhs_ub - rhs)
-                var_added = True
-        else:
-            raise QiskitOptimizationError("The type of Sense in {} is not supported".format(name))
-
-        # Add a new equality constraint.
-        new_linear = copy.deepcopy(linear)
-        if var_added:
             new_linear[slack_name] = sign
-        self._dst.quadratic_constraint(new_linear, quadratic, "==", rhs, name)
-
-    def _add_auto_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name):
-        # If a coefficient that is not integer exist, use a continuous slack variable
-        if self._contains_any_float_value(list(linear.values())) or self._contains_any_float_value(
-            list(quadratic.values())
-        ):
-            self._add_continuous_slack_var_quadratic_constraint(linear, quadratic, sense, rhs, name)
-        # Else use an integer slack variable
-        else:
-            self._add_integer_slack_var_quadratic_constraint(linear, quadratic, sense, rhs, name)
-
-    def _calc_linear_bounds(self, linear):
-        lhs_lb, lhs_ub = 0, 0
-        for var_name, v in linear.items():
-            x = self._src.get_variable(var_name)
-            lhs_lb += min(x.lowerbound * v, x.upperbound * v)
-            lhs_ub += max(x.lowerbound * v, x.upperbound * v)
-        return lhs_lb, lhs_ub
-
-    def _calc_quadratic_bounds(self, linear, quadratic):
-        lhs_lb, lhs_ub = 0, 0
-        # Calculate the lowerbound and the upperbound of the linear part
-        linear_lb, linear_ub = self._calc_linear_bounds(linear)
-        lhs_lb += linear_lb
-        lhs_ub += linear_ub
-
-        # Calculate the lowerbound and the upperbound of the quadratic part
-        for (name_i, name_j), v in quadratic.items():
-            x = self._src.get_variable(name_i)
-            y = self._src.get_variable(name_j)
-
-            lhs_lb += min(
-                x.lowerbound * y.lowerbound * v,
-                x.lowerbound * y.upperbound * v,
-                x.upperbound * y.lowerbound * v,
-                x.upperbound * y.upperbound * v,
-            )
-            lhs_ub += max(
-                x.lowerbound * y.lowerbound * v,
-                x.lowerbound * y.upperbound * v,
-                x.upperbound * y.lowerbound * v,
-                x.upperbound * y.upperbound * v,
-            )
-        return lhs_lb, lhs_ub
+        self._dst.quadratic_constraint(new_linear, quadratic.coefficients, "==", new_rhs, name)
 
     def interpret(self, x: Union[np.ndarray, List[float]]) -> np.ndarray:
         """Convert a result of a converted problem into that of the original problem.
@@ -390,7 +257,7 @@ class InequalityToEquality(QuadraticProgramConverter):
         return new_x
 
     @staticmethod
-    def _contains_any_float_value(values: List[Union[int, float]]) -> bool:
+    def _any_float(values: np.ndarray) -> bool:
         """Check whether the list contains float or not.
         This method is used to check whether a constraint contain float coefficients or not.
 
@@ -400,7 +267,7 @@ class InequalityToEquality(QuadraticProgramConverter):
         Returns:
             bool: If the constraint contains float coefficients, this returns True, else False.
         """
-        return any(isinstance(v, float) and not v.is_integer() for v in values)  # type: ignore
+        return any(isinstance(v, float) and not cast(float, v).is_integer() for v in values)
 
     @property
     def mode(self) -> str:
diff --git a/qiskit_optimization/converters/linear_equality_to_penalty.py b/qiskit_optimization/converters/linear_equality_to_penalty.py
index 4d507b5..05a26ad 100644
--- a/qiskit_optimization/converters/linear_equality_to_penalty.py
+++ b/qiskit_optimization/converters/linear_equality_to_penalty.py
@@ -13,7 +13,6 @@
 """Converter to convert a problem with equality constraints to unconstrained with penalty terms."""
 
 import logging
-from math import fsum
 from typing import Optional, cast, Union, Tuple, List
 
 import numpy as np
@@ -142,7 +141,7 @@ class LinearEqualityToPenalty(QuadraticProgramConverter):
         terms = []
         for constraint in problem.linear_constraints:
             terms.append(constraint.rhs)
-            terms.extend(coef for coef in constraint.linear.to_dict().values())
+            terms.extend(constraint.linear.to_array().tolist())
         if any(isinstance(term, float) and not term.is_integer() for term in terms):
             logger.warning(
                 "Warning: Using %f for the penalty coefficient because "
@@ -153,15 +152,9 @@ class LinearEqualityToPenalty(QuadraticProgramConverter):
             )
             return default_penalty
 
-        # (upper bound - lower bound) can be calculate as the sum of absolute value of coefficients
-        # Firstly, add 1 to guarantee that infeasible answers will be greater than upper bound.
-        penalties = [1.0]
-        # add linear terms of the object function.
-        penalties.extend(abs(coef) for coef in problem.objective.linear.to_dict().values())
-        # add quadratic terms of the object function.
-        penalties.extend(abs(coef) for coef in problem.objective.quadratic.to_dict().values())
-
-        return fsum(penalties)
+        lin_b = problem.objective.linear.bounds
+        quad_b = problem.objective.quadratic.bounds
+        return 1.0 + (lin_b.upperbound - lin_b.lowerbound) + (quad_b.upperbound - quad_b.lowerbound)
 
     def interpret(self, x: Union[np.ndarray, List[float]]) -> np.ndarray:
         """Convert the result of the converted problem back to that of the original problem
diff --git a/qiskit_optimization/problems/linear_expression.py b/qiskit_optimization/problems/linear_expression.py
index 5ff5468..cceea7b 100644
--- a/qiskit_optimization/problems/linear_expression.py
+++ b/qiskit_optimization/problems/linear_expression.py
@@ -13,12 +13,25 @@
 """Linear expression interface."""
 
 from typing import List, Union, Dict, Any
+from dataclasses import dataclass
 
 from numpy import ndarray
 from scipy.sparse import spmatrix, dok_matrix
 
 from .quadratic_program_element import QuadraticProgramElement
 from ..exceptions import QiskitOptimizationError
+from ..infinity import INFINITY
+
+
+@dataclass
+class ExpressionBounds:
+    """Lower bound and upper bound of a linear expression or a quadratic expression"""
+
+    lowerbound: float
+    """Lower bound"""
+
+    upperbound: float
+    """Upper bound"""
 
 
 class LinearExpression(QuadraticProgramElement):
@@ -172,3 +185,26 @@ class LinearExpression(QuadraticProgramElement):
 
         # extract the coefficients as array and return it
         return self.to_array()
+
+    @property
+    def bounds(self) -> ExpressionBounds:
+        """Returns the lower bound and the upper bound of the linear expression
+
+        Returns:
+            The lower bound and the upper bound of the linear expression
+
+        Raises:
+            QiskitOptimizationError: if the linear expression contains any unbounded variable
+
+        """
+        l_b = u_b = 0.0
+        for ind, coeff in self.to_dict().items():
+            x = self.quadratic_program.get_variable(ind)
+            if x.lowerbound == -INFINITY or x.upperbound == INFINITY:
+                raise QiskitOptimizationError(
+                    f"Linear expression contains an unbounded variable: {x.name}"
+                )
+            lst = [coeff * x.lowerbound, coeff * x.upperbound]
+            l_b += min(lst)
+            u_b += max(lst)
+        return ExpressionBounds(lowerbound=l_b, upperbound=u_b)
diff --git a/qiskit_optimization/problems/quadratic_expression.py b/qiskit_optimization/problems/quadratic_expression.py
index e968264..a5cde4d 100644
--- a/qiskit_optimization/problems/quadratic_expression.py
+++ b/qiskit_optimization/problems/quadratic_expression.py
@@ -20,6 +20,8 @@ from scipy.sparse import spmatrix, dok_matrix, tril, triu
 
 from .quadratic_program_element import QuadraticProgramElement
 from ..exceptions import QiskitOptimizationError
+from ..infinity import INFINITY
+from .linear_expression import ExpressionBounds
 
 
 class QuadraticExpression(QuadraticProgramElement):
@@ -241,3 +243,45 @@ class QuadraticExpression(QuadraticProgramElement):
         if isinstance(x, list):
             x = np.array(x)
         return x
+
+    @property
+    def bounds(self) -> ExpressionBounds:
+        """Returns the lower bound and the upper bound of the quadratic expression
+
+        Returns:
+            The lower bound and the upper bound of the quadratic expression
+
+        Raises:
+            QiskitOptimizationError: if the quadratic expression contains any unbounded variable
+        """
+        l_b = u_b = 0.0
+        for (ind1, ind2), coeff in self.to_dict().items():
+            x = self.quadratic_program.get_variable(ind1)
+            if x.lowerbound == -INFINITY or x.upperbound == INFINITY:
+                raise QiskitOptimizationError(
+                    f"Quadratic expression contains an unbounded variable: {x.name}"
+                )
+            y = self.quadratic_program.get_variable(ind2)
+            if y.lowerbound == -INFINITY or y.upperbound == INFINITY:
+                raise QiskitOptimizationError(
+                    f"Quadratic expression contains an unbounded variable: {y.name}"
+                )
+            lst = []
+            if ind1 == ind2:
+                if x.lowerbound * x.upperbound <= 0.0:
+                    # lower bound and upper bound have different signs
+                    lst.append(0.0)
+                lst.extend([x.lowerbound ** 2, x.upperbound ** 2])
+            else:
+                lst.extend(
+                    [
+                        x.lowerbound * y.lowerbound,
+                        x.lowerbound * y.upperbound,
+                        x.upperbound * y.lowerbound,
+                        x.upperbound * y.upperbound,
+                    ]
+                )
+            lst2 = [coeff * val for val in lst]
+            l_b += min(lst2)
+            u_b += max(lst2)
+        return ExpressionBounds(lowerbound=l_b, upperbound=u_b)
diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py
index d36abc6..bcb57b6 100644
--- a/qiskit_optimization/problems/quadratic_program.py
+++ b/qiskit_optimization/problems/quadratic_program.py
@@ -533,7 +533,7 @@ class QuadraticProgram:
         Returns:
             The corresponding variable.
         """
-        if isinstance(i, int):
+        if isinstance(i, (int, np.integer)):
             return self.variables[i]
         else:
             return self.variables[self._variables_index[i]]
diff --git a/releasenotes/notes/add-bounds-expressions-3eeb6d3c06414b31.yaml b/releasenotes/notes/add-bounds-expressions-3eeb6d3c06414b31.yaml
new file mode 100644
index 0000000..d152da0
--- /dev/null
+++ b/releasenotes/notes/add-bounds-expressions-3eeb6d3c06414b31.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds :meth:`qiskit_optimization.problems.LinearExpression.bounds` and
+    :meth:`qiskit_optimization.problems.QuadraticExpression.bounds` that
+    return the lower bound and the upper bound of the expressions.


Test patch:
diff --git a/test/problems/test_linear_expression.py b/test/problems/test_linear_expression.py
index a11d24b..0a77bba 100644
--- a/test/problems/test_linear_expression.py
+++ b/test/problems/test_linear_expression.py
@@ -13,12 +13,12 @@
 """ Test LinearExpression """
 
 import unittest
-
 from test.optimization_test_case import QiskitOptimizationTestCase
+
 import numpy as np
 from scipy.sparse import dok_matrix
 
-from qiskit_optimization import QuadraticProgram
+from qiskit_optimization import INFINITY, QiskitOptimizationError, QuadraticProgram
 from qiskit_optimization.problems import LinearExpression
 
 
@@ -126,6 +126,53 @@ class TestLinearExpression(QiskitOptimizationTestCase):
         for values in [values_list, values_array, values_dict_int, values_dict_str]:
             np.testing.assert_almost_equal(linear.evaluate_gradient(values), coefficients_list)
 
+    def test_bounds(self):
+        """test lowerbound and upperbound"""
+
+        with self.subTest("bounded"):
+            quadratic_program = QuadraticProgram()
+            quadratic_program.continuous_var_list(3, lowerbound=-1, upperbound=2)
+            coefficients_list = list(range(3))
+            bounds = LinearExpression(quadratic_program, coefficients_list).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -3)
+            self.assertAlmostEqual(bounds.upperbound, 6)
+
+        with self.subTest("bounded2"):
+            quadratic_program = QuadraticProgram()
+            quadratic_program.integer_var(lowerbound=-2, upperbound=-1, name="x")
+            quadratic_program.integer_var(lowerbound=2, upperbound=4, name="y")
+            bounds = LinearExpression(quadratic_program, {"x": 1, "y": 10}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 18)
+            self.assertAlmostEqual(bounds.upperbound, 39)
+
+            bounds = LinearExpression(quadratic_program, {"x": -1, "y": 10}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 21)
+            self.assertAlmostEqual(bounds.upperbound, 42)
+
+            bounds = LinearExpression(quadratic_program, {"x": 1, "y": -10}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -42)
+            self.assertAlmostEqual(bounds.upperbound, -21)
+
+            bounds = LinearExpression(quadratic_program, {"x": -1, "y": -10}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -39)
+            self.assertAlmostEqual(bounds.upperbound, -18)
+
+            bounds = LinearExpression(quadratic_program, {"x": 0, "y": 0}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 0)
+            self.assertAlmostEqual(bounds.upperbound, 0)
+
+        with self.assertRaises(QiskitOptimizationError):
+            quadratic_program = QuadraticProgram()
+            quadratic_program.continuous_var_list(3, lowerbound=0, upperbound=INFINITY)
+            coefficients_list = list(range(3))
+            _ = LinearExpression(quadratic_program, coefficients_list).bounds
+
+        with self.assertRaises(QiskitOptimizationError):
+            quadratic_program = QuadraticProgram()
+            quadratic_program.continuous_var_list(3, lowerbound=-INFINITY, upperbound=0)
+            coefficients_list = list(range(3))
+            _ = LinearExpression(quadratic_program, coefficients_list).bounds
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/problems/test_quadratic_expression.py b/test/problems/test_quadratic_expression.py
index 830eb90..e405f58 100644
--- a/test/problems/test_quadratic_expression.py
+++ b/test/problems/test_quadratic_expression.py
@@ -13,12 +13,12 @@
 """ Test QuadraticExpression """
 
 import unittest
-
 from test.optimization_test_case import QiskitOptimizationTestCase
+
 import numpy as np
 from scipy.sparse import dok_matrix
 
-from qiskit_optimization import QuadraticProgram
+from qiskit_optimization import INFINITY, QiskitOptimizationError, QuadraticProgram
 from qiskit_optimization.problems import QuadraticExpression
 
 
@@ -164,6 +164,78 @@ class TestQuadraticExpression(QiskitOptimizationTestCase):
             {("x", "y"): 0.5, ("y", "x"): 0.5, ("x", "z"): 1.5, ("z", "x"): 1.5},
         )
 
+    def test_bounds(self):
+        """test lowerbound and upperbound"""
+
+        with self.subTest("bounded"):
+            q_p = QuadraticProgram()
+            q_p.continuous_var(1, 2, "x")
+            q_p.continuous_var(-2, -1, "y")
+            q_p.continuous_var(-1, 2, "z")
+            bounds = QuadraticExpression(q_p, {("x", "y"): 1, ("y", "z"): 2, ("z", "z"): 3}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -12)
+            self.assertAlmostEqual(bounds.upperbound, 15)
+
+        with self.subTest("bounded2"):
+            q_p = QuadraticProgram()
+            q_p.integer_var(-1, 2, "x")
+            bounds = QuadraticExpression(q_p, {("x", "x"): 1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 0)
+            self.assertAlmostEqual(bounds.upperbound, 4)
+
+            bounds = QuadraticExpression(q_p, {("x", "x"): -1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -4)
+            self.assertAlmostEqual(bounds.upperbound, 0)
+
+            bounds = QuadraticExpression(q_p, {("x", "x"): 0}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 0)
+            self.assertAlmostEqual(bounds.upperbound, 0)
+
+        with self.subTest("bounded3"):
+            q_p = QuadraticProgram()
+            q_p.integer_var(-2, -1, "x")
+            bounds = QuadraticExpression(q_p, {("x", "x"): 1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 1)
+            self.assertAlmostEqual(bounds.upperbound, 4)
+
+            bounds = QuadraticExpression(q_p, {("x", "x"): -1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -4)
+            self.assertAlmostEqual(bounds.upperbound, -1)
+
+            bounds = QuadraticExpression(q_p, {("x", "x"): 0}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 0)
+            self.assertAlmostEqual(bounds.upperbound, 0)
+
+        with self.subTest("bounded4"):
+            q_p = QuadraticProgram()
+            q_p.integer_var(-2, 3, "x")
+            q_p.integer_var(-10, 20, "y")
+            bounds = QuadraticExpression(q_p, {("x", "y"): 1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -40)
+            self.assertAlmostEqual(bounds.upperbound, 60)
+
+            bounds = QuadraticExpression(q_p, {("x", "y"): -1}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, -60)
+            self.assertAlmostEqual(bounds.upperbound, 40)
+
+            bounds = QuadraticExpression(q_p, {("x", "y"): 0}).bounds
+            self.assertAlmostEqual(bounds.lowerbound, 0)
+            self.assertAlmostEqual(bounds.upperbound, 0)
+
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.continuous_var(1, 2, "x")
+            q_p.continuous_var(-2, -1, "y")
+            q_p.continuous_var(-INFINITY, 2, "z")
+            _ = QuadraticExpression(q_p, {("x", "y"): 1, ("y", "z"): 2, ("z", "z"): 3}).bounds
+
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.continuous_var(1, 2, "x")
+            q_p.continuous_var(-2, -1, "y")
+            q_p.continuous_var(-1, INFINITY, "z")
+            _ = QuadraticExpression(q_p, {("x", "y"): 1, ("y", "z"): 2, ("z", "z"): 3}).bounds
+
 
 if __name__ == "__main__":
     unittest.main()