Repository: Qiskit/qiskit-optimization
Pull request #140 (2021-05-20 08:54:36)

Raise an appropriate error if an empty objective function is used
<!--
⚠️ 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

An error occurs if no objective function is set. This PR fixes it.

```python
from qiskit_optimization import QuadraticProgram

qp = QuadraticProgram()
qp.binary_var_list(3)
print(qp.objective.evaluate([0, 0, 0]))
```

```
# before
Traceback (most recent call last):
  File "obj2.py", line 5, in <module>
    print(qp.objective.evaluate([0, 0, 0]))
  File "/Users/ima/envs/qiskit38/lib/python3.8/site-packages/qiskit_optimization/problems/quadratic_objective.py", line 151, in evaluate
    return self.constant + self.linear.evaluate(x) + self.quadratic.evaluate(x)
  File "/Users/ima/envs/qiskit38/lib/python3.8/site-packages/qiskit_optimization/problems/linear_expression.py", line 151, in evaluate
    val = (x @ self.coefficients.transpose())[0, 0]
  File "/Users/ima/envs/qiskit38/lib/python3.8/site-packages/scipy/sparse/base.py", line 560, in __matmul__
    return self.__mul__(other)
  File "/Users/ima/envs/qiskit38/lib/python3.8/site-packages/cvxpy/interface/scipy_wrapper.py", line 31, in new_method
    return method(self, other)
  File "/Users/ima/envs/qiskit38/lib/python3.8/site-packages/scipy/sparse/base.py", line 479, in __mul__
    raise ValueError('dimension mismatch')
ValueError: dimension mismatch
```

```
# after
Traceback (most recent call last):
  File "obj2.py", line 7, in <module>
    print(qp.objective.evaluate([0, 0, 0]))
  File "/Users/ima/tasks/1_2021/qiskit/opt/qiskit_optimization/problems/quadratic_objective.py", line 173, in evaluate
    raise QiskitOptimizationError(
qiskit_optimization.exceptions.QiskitOptimizationError: 'The shape of the objective function does not match with the number of variables. Need to define the objective function after defining all variables'
```

### Details and comments




t-imamichi: Thanks. Your idea makes sense. Let me try it.
t-imamichi: The unit test of objective assumes that the objective is not zero if not set.
https://github.com/Qiskit/qiskit-optimization/blob/93b322f954972dea11a0d4a0957c79d28648e313/test/problems/test_quadratic_objective.py#L26-L36

There are two options.
1. raise an error if users does not set objective and access it. We need to modify the unit tests of objective too.
2. adjust shape of expressions in the objective as I did in this PR.

@adekusar-drl Which do you prefer? I'm ok to make the access of objective error if it is not set.
t-imamichi: Thank you for your idea. I simplified it by raising an error if no obj is set.
t-imamichi: Thank you!
adekusar-drl: ping @manoelmarques for actual approval :)
woodsp-ibm: Just a question - do we really want/need to backport this. It seems to be as much an improvement to detect a mismatch between obj fn and vars that a user can easily avoid at present - ie its not really a show stopping/critical bug in stable that is more the criteria for backporting.
t-imamichi: Thank you for your feedback. You are right. It is not a critical bug.

Patch:
diff --git a/qiskit_optimization/problems/quadratic_objective.py b/qiskit_optimization/problems/quadratic_objective.py
index e602ec4..022281d 100644
--- a/qiskit_optimization/problems/quadratic_objective.py
+++ b/qiskit_optimization/problems/quadratic_objective.py
@@ -18,6 +18,7 @@ from typing import Union, List, Dict, Tuple, Any
 from numpy import ndarray
 from scipy.sparse import spmatrix
 
+from ..exceptions import QiskitOptimizationError
 from .linear_constraint import LinearExpression
 from .quadratic_expression import QuadraticExpression
 from .quadratic_program_element import QuadraticProgramElement
@@ -162,7 +163,17 @@ class QuadraticObjective(QuadraticProgramElement):
 
         Returns:
             The value of the quadratic objective given the variable values.
+
+        Raises:
+            QiskitOptimizationError: if the shape of the objective function does not match with
+                the number of variables.
         """
+        n = self.quadratic_program.get_num_vars()
+        if self.linear.coefficients.shape != (1, n) or self.quadratic.coefficients.shape != (n, n):
+            raise QiskitOptimizationError(
+                "The shape of the objective function does not match with the number of variables. "
+                "Need to define the objective function after defining all variables"
+            )
         return self.constant + self.linear.evaluate(x) + self.quadratic.evaluate(x)
 
     def evaluate_gradient(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> ndarray:
@@ -173,5 +184,15 @@ class QuadraticObjective(QuadraticProgramElement):
 
         Returns:
             The value of the gradient of the quadratic objective given the variable values.
+
+        Raises:
+            QiskitOptimizationError: if the shape of the objective function does not match with
+                the number of variables.
         """
+        n = self.quadratic_program.get_num_vars()
+        if self.linear.coefficients.shape != (1, n) or self.quadratic.coefficients.shape != (n, n):
+            raise QiskitOptimizationError(
+                "The shape of the objective function does not match with the number of variables. "
+                "Need to define the objective function after defining all variables"
+            )
         return self.linear.evaluate_gradient(x) + self.quadratic.evaluate_gradient(x)
diff --git a/releasenotes/notes/fix-empty-obj-3a99dc67c319806d.yaml b/releasenotes/notes/fix-empty-obj-3a99dc67c319806d.yaml
new file mode 100644
index 0000000..1bab023
--- /dev/null
+++ b/releasenotes/notes/fix-empty-obj-3a99dc67c319806d.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fixes :meth`qiskit_optimization.problems.quadratic_objective.QuadraticObjective.evaluate`
+    and :meth`qiskit_optimization.problems.quadratic_objective.QuadraticObjective.evaluate_gradient`
+    to raise ``QiskitOptimizationError`` with an appropriate message if no objective function is set.


Test patch:
diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py
index 6119c1d..03831d3 100644
--- a/test/problems/test_quadratic_program.py
+++ b/test/problems/test_quadratic_program.py
@@ -15,20 +15,13 @@
 import tempfile
 import unittest
 from os import path
-from test.optimization_test_case import (
-    QiskitOptimizationTestCase,
-    requires_extra_library,
-)
+from test.optimization_test_case import QiskitOptimizationTestCase, requires_extra_library
 
+import numpy as np
 from docplex.mp.model import DOcplexException, Model
 
 from qiskit_optimization import INFINITY, QiskitOptimizationError, QuadraticProgram
-from qiskit_optimization.problems import (
-    Constraint,
-    QuadraticObjective,
-    Variable,
-    VarType,
-)
+from qiskit_optimization.problems import Constraint, QuadraticObjective, Variable, VarType
 
 
 class TestQuadraticProgram(QiskitOptimizationTestCase):
@@ -725,6 +718,28 @@ class TestQuadraticProgram(QiskitOptimizationTestCase):
             [[0, 0, 0.5], [0, 1, 0], [0.5, 0, 0]],
         )
 
+    def test_empty_objective(self):
+        """test empty objective"""
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.binary_var_list(3)
+            _ = q_p.objective.evaluate([0, 0, 0])
+
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.binary_var_list(3)
+            _ = q_p.objective.evaluate_gradient([0, 0, 0])
+
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.binary_var_list(3)
+            _ = q_p.objective.evaluate({})
+
+        with self.assertRaises(QiskitOptimizationError):
+            q_p = QuadraticProgram()
+            q_p.binary_var_list(3)
+            _ = q_p.objective.evaluate_gradient({})
+
     @requires_extra_library
     def test_read_from_lp_file(self):
         """test read lp file"""