Repository: fitbenchmarking/fitbenchmarking

Issue:
Poisson cost function not compatible with analytic jacobian
**Description of the error**
When running fitbenchmarking with poisson cost function and analytic jacobian selected, for minimizers that accept jacobian information the following error message is output for each minimizer:
`'PoissonCostFunc' object has no attribute 'cache_rx'`

Because the poisson cost function is not a least squares problem it has no `cache_rx` attribute or `eval_r` function, so the following line in `analytic_jacobian` causes the issue:
```
rx = self.cached_func_values(self.cost_func.cache_rx,
                                     self.cost_func.eval_r,
                                     params,
                                     **kwargs)
```
An alternative to this line should be used for cost functions that are not least squares problems.

Discussion:

Patch:
diff --git a/fitbenchmarking/jacobian/analytic_jacobian.py b/fitbenchmarking/jacobian/analytic_jacobian.py
index e0d3a135..0b5ee00e 100644
--- a/fitbenchmarking/jacobian/analytic_jacobian.py
+++ b/fitbenchmarking/jacobian/analytic_jacobian.py
@@ -1,8 +1,7 @@
 """
 Module which acts as a analytic Jacobian calculator
 """
-from numpy import matmul
-from numpy import sqrt
+import numpy as np
 
 from fitbenchmarking.jacobian.base_jacobian import Jacobian
 from fitbenchmarking.utils.exceptions import NoJacobianError
@@ -39,9 +38,12 @@ class Analytic(Jacobian):
             jac = jac / e[:, None]
         elif self.problem.options.cost_func_type == "hellinger_nlls":
             # calculates the Jacobian of the hellinger(root) NLLS cost function
-            jac = jac/(2*sqrt(self.problem.eval_model(params, x=x)[:, None]))
+            jac = jac/(2*np.sqrt(self.problem.eval_model(params, x=x)
+                       [:, None]))
         elif self.problem.options.cost_func_type == "poisson":
-            jac = jac * (1 - y / self.problem.eval_model(params, x=x))[:, None]
+            jac = -np.matmul(jac.T,
+                             (1 - y / self.problem.eval_model(params, x=x)))
+
         return jac
 
     def eval_cost(self, params, **kwargs):
@@ -54,10 +56,14 @@ class Analytic(Jacobian):
         :return: Computed derivative of the cost function
         :rtype: numpy array
         """
-        rx = self.cached_func_values(self.cost_func.cache_rx,
-                                     self.cost_func.eval_r,
-                                     params,
-                                     **kwargs)
         J = self.eval(params, **kwargs)
-        out = 2.0 * matmul(J.T, rx)
+        if self.problem.options.cost_func_type == "poisson":
+            out = J
+        else:
+            rx = self.cached_func_values(self.cost_func.cache_rx,
+                                         self.cost_func.eval_r,
+                                         params,
+                                         **kwargs)
+            out = 2.0 * np.matmul(J.T, rx)
+
         return out


Test patch:
diff --git a/fitbenchmarking/jacobian/tests/test_jacobian.py b/fitbenchmarking/jacobian/tests/test_jacobian.py
index c69206ac..5b0c2e5a 100644
--- a/fitbenchmarking/jacobian/tests/test_jacobian.py
+++ b/fitbenchmarking/jacobian/tests/test_jacobian.py
@@ -163,9 +163,10 @@ class TestJacobianClass(TestCase):
         self.fitting_problem.format = "cutest"
         jac = Analytic(self.cost_func)
         eval_result = jac.eval(params=self.params)
-        scaled_actual = self.actual * \
-            (1 - self.fitting_problem.data_y /
-                f(self.fitting_problem.data_x, *self.params))[:, None]
+        scaled_actual = -np.matmul(self.actual.T,
+                                   (1 - self.fitting_problem.data_y /
+                                    f(self.fitting_problem.data_x,
+                                      *self.params)))
 
         self.assertTrue(np.isclose(scaled_actual, eval_result).all())