Repository: fitbenchmarking/fitbenchmarking

Issue:
Performance profiler fails if 2 minimizers share a name
**Description of the error**
When running scipy and mantid, the performance profiler fails due to both having a minimizer called BFGS.


Discussion:
github-actions[bot]: ## Unit & System Test Results
    4 files  ±0      4 suites  ±0   6m 42s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") + 1m 17s
350 tests  - 2  316 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests")  - 29    7 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  27 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") +27 
668 runs   - 4  572 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests")  - 55  45 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  51 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") +51 

For more details on these failures, see [this check](https://github.com/fitbenchmarking/fitbenchmarking/runs/4536017081).

Results for commit 5cd5eb8f. ± Comparison against base commit ea398efa.

<details>
  <summary>This pull request <b>removes</b> 2 tests.</summary>

```
fitbenchmarking.core.tests.test_fitting_benchmarking_hessians.LoopOverHessiansTests ‑ test_single_no_hessian
fitbenchmarking.core.tests.test_fitting_benchmarking_jacobians.LoopOverJacobiansTests ‑ test_single_no_jacobian
```
</details>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/45037208/badge)](https://coveralls.io/builds/45037208)

Coverage decreased (-0.1%) to 87.61% when pulling **aba9ff0039045f1c072a9dd8778c53469f310e40 on 1009-refactor_minimzer_names** into **ea398efa61f071dc64fe7c3b484d5bb4e1897856 on master**.


Patch:
diff --git a/fitbenchmarking/core/fitting_benchmarking.py b/fitbenchmarking/core/fitting_benchmarking.py
index 89ff84ab..428f18fd 100644
--- a/fitbenchmarking/core/fitting_benchmarking.py
+++ b/fitbenchmarking/core/fitting_benchmarking.py
@@ -66,12 +66,9 @@ def benchmark(options, data_dir):
     #################################
     # Loops over benchmark problems #
     #################################
-    results, failed_problems, unselected_minimizers, \
-        minimizer_dict = \
+    results, failed_problems, unselected_minimizers = \
         loop_over_benchmark_problems(problem_group, options)
 
-    options.minimizers = minimizer_dict
-
     return results, failed_problems, unselected_minimizers
 
 
@@ -85,19 +82,16 @@ def loop_over_benchmark_problems(problem_group, options):
     :type options: fitbenchmarking.utils.options.Options
 
     :return: all results,
-             problems where all fitting failed
-             minimizers that were unselected due to algorithm_type, and
-             composite minimizer-jacobian-hessian names grouped by software
+             problems where all fitting failed, and
+             minimizers that were unselected due to algorithm_type
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
             list[str],
-            dict[str, list[str]],
             dict[str, list[str]]
     """
     grabbed_output = output_grabber.OutputGrabber(options)
     results = []
     failed_problems = []
     unselected_minimizers = []
-    minimizer_dict = []
 
     LOGGER.info('Parsing problems')
     problems = []
@@ -133,14 +127,13 @@ def loop_over_benchmark_problems(problem_group, options):
         # Loops over starting values #
         ##############################
         problem_results, problem_fails, \
-            unselected_minimizers, minimizer_dict = \
+            unselected_minimizers = \
             loop_over_starting_values(
                 problem, options, grabbed_output)
         results.extend(problem_results)
         failed_problems.extend(problem_fails)
 
-    return results, failed_problems, unselected_minimizers, \
-        minimizer_dict
+    return results, failed_problems, unselected_minimizers
 
 
 def loop_over_starting_values(problem, options, grabbed_output):
@@ -155,12 +148,10 @@ def loop_over_starting_values(problem, options, grabbed_output):
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
     :return: all results,
-             problems where all fitting failed
-             minimizers that were unselected due to algorithm_type, and
-             composite minimizer-jacobian-hessian names grouped by software
+             problems where all fitting failed, and
+             minimizers that were unselected due to algorithm_type
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
             list[str],
-            dict[str, list[str]],
             dict[str, list[str]]
     """
     problem_fails = []
@@ -175,7 +166,7 @@ def loop_over_starting_values(problem, options, grabbed_output):
         #############################
         # Loops over cost functions #
         #############################
-        individual_problem_results, unselected_minimizers, minimizer_dict = \
+        individual_problem_results, unselected_minimizers = \
             loop_over_cost_function(problem=problem,
                                     options=options,
                                     start_values_index=index,
@@ -193,7 +184,7 @@ def loop_over_starting_values(problem, options, grabbed_output):
         problem.name = name
 
     return (problem_results, problem_fails,
-            unselected_minimizers, minimizer_dict)
+            unselected_minimizers)
 
 
 def loop_over_cost_function(problem, options, start_values_index,
@@ -211,11 +202,9 @@ def loop_over_cost_function(problem, options, start_values_index,
     :param grabbed_output: Object that removes third part output from console
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
-    :return: all results,
-             minimizers that were unselected due to algorithm_type, and
-             composite minimizer-jacobian-hessian names grouped by software
+    :return: all results, and
+             minimizers that were unselected due to algorithm_type
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
-            dict[str, list[str]],
             dict[str, list[str]]
     """
     problem_results = []
@@ -225,14 +214,14 @@ def loop_over_cost_function(problem, options, start_values_index,
         #######################
         # Loops over software #
         #######################
-        individual_problem_results, unselected_minimizers, minimizer_dict = \
+        individual_problem_results, unselected_minimizers = \
             loop_over_fitting_software(cost_func=cost_func,
                                        options=options,
                                        start_values_index=start_values_index,
                                        grabbed_output=grabbed_output)
         problem_results.extend(individual_problem_results)
 
-    return problem_results, unselected_minimizers, minimizer_dict
+    return problem_results, unselected_minimizers
 
 
 def loop_over_fitting_software(cost_func, options, start_values_index,
@@ -250,11 +239,9 @@ def loop_over_fitting_software(cost_func, options, start_values_index,
     :param grabbed_output: Object that removes third part output from console
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
-    :return: all results,
-             minimizers that were unselected due to algorithm_type, and
-             composite minimizer-jacobian-hessian names grouped by software
+    :return: all results, and
+             minimizers that were unselected due to algorithm_type
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
-            dict[str, list[str]],
             dict[str, list[str]]
     """
     results = []
@@ -263,7 +250,6 @@ def loop_over_fitting_software(cost_func, options, start_values_index,
         software = [software]
 
     unselected_minimizers = {}
-    minimizer_dict = {}
     for s in software:
         LOGGER.info("        Software: %s", s.upper())
         try:
@@ -281,16 +267,15 @@ def loop_over_fitting_software(cost_func, options, start_values_index,
         #########################
         # Loops over minimizers #
         #########################
-        problem_result, minimizer_failed, new_minimizer_list = \
+        problem_result, minimizer_failed = \
             loop_over_minimizers(controller=controller,
                                  minimizers=minimizers,
                                  options=options,
                                  grabbed_output=grabbed_output)
 
         unselected_minimizers[s] = minimizer_failed
-        minimizer_dict[s] = new_minimizer_list
         results.extend(problem_result)
-    return results, unselected_minimizers, minimizer_dict
+    return results, unselected_minimizers
 
 
 def loop_over_minimizers(controller, minimizers, options, grabbed_output):
@@ -306,11 +291,9 @@ def loop_over_minimizers(controller, minimizers, options, grabbed_output):
     :param grabbed_output: Object that removes third part output from console
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
-    :return: all results,
-             minimizers that were unselected due to algorithm_type, and
-             composite minimizer-jacobian-hessian names
+    :return: all results, and
+             minimizers that were unselected due to algorithm_type
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
-            list[str],
             list[str])
     """
     problem = controller.problem
@@ -318,7 +301,6 @@ def loop_over_minimizers(controller, minimizers, options, grabbed_output):
 
     results_problem = []
     minimizer_failed = []
-    new_minimizer_list = []
     for minimizer in minimizers:
         controller.minimizer = minimizer
         minimizer_check = True
@@ -345,42 +327,32 @@ def loop_over_minimizers(controller, minimizers, options, grabbed_output):
                 if minimizer_check:
                     minimizer_check = False
                     controller.flag = 4
-                    dummy_results =\
-                        [{'options': options,
-                          'cost_func': controller.cost_func,
-                          'jac': None,
-                          'hess': None,
-                          'chi_sq': np.inf,
-                          'runtime': np.inf,
-                          'software': controller.software,
-                          'minimizer': minimizer,
-                          'initial_params': controller.initial_params,
-                          'params': None,
-                          'error_flag': controller.flag,
-                          'name': problem.name}]
-                    # record algorithm type for specified minimizer
-                    type_str = controller.record_alg_type(
-                        minimizer, options.algorithm_type)
-                    options.minimizer_alg_type[minimizer] = type_str
-                    for result in dummy_results:
-                        individual_result = fitbm_result.FittingResult(
-                            **result)
-                        results_problem.append(individual_result)
-                    new_minimizer_list.append(minimizer)
+                    dummy_result = fitbm_result.FittingResult(
+                        options=options,
+                        cost_func=controller.cost_func,
+                        jac=None,
+                        hess=None,
+                        chi_sq=np.inf,
+                        runtime=np.inf,
+                        software=controller.software,
+                        minimizer=minimizer,
+                        algorithm_type=controller.record_alg_type(
+                           minimizer, options.algorithm_type),
+                        initial_params=controller.initial_params,
+                        params=None,
+                        error_flag=controller.flag,
+                        name=problem.name)
+                    results_problem.append(dummy_result)
                     LOGGER.warning(str(excp))
 
         if minimizer_check:
             ########################
             # Loops over Jacobians #
             ########################
-            results, minimizer_list = loop_over_jacobians(controller,
-                                                          options,
-                                                          grabbed_output)
-
+            results = loop_over_jacobians(controller, options, grabbed_output)
             results_problem.extend(results)
-            new_minimizer_list.extend(minimizer_list)
 
-    return results_problem, minimizer_failed, new_minimizer_list
+    return results_problem, minimizer_failed
 
 
 def loop_over_jacobians(controller, options, grabbed_output):
@@ -394,17 +366,13 @@ def loop_over_jacobians(controller, options, grabbed_output):
     :param grabbed_output: Object that removes third part output from console
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
-    :return: all results as a dictionary of arguments for FittingResult, and
-             composite minimizer-jacobian-hessian names.
-    :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
-            list[str]
+    :return: a FittingResult for each run.
+    :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult]
     """
     cost_func = controller.cost_func
     minimizer = controller.minimizer
     jacobian_list = options.jac_method
-    minimizer_name = minimizer
     results = []
-    minimizer_list = []
     minimizer_check = minimizer in controller.jacobian_enabled_solvers
     try:
         for jac_method in jacobian_list:
@@ -418,25 +386,21 @@ def loop_over_jacobians(controller, options, grabbed_output):
                 continue
 
             for num_method in options.jac_num_method[jac_method]:
-                if minimizer_check:
-                    num_method_str = ''
-                    if jac_method != "analytic":
-                        num_method_str = ' ' + num_method
-                    LOGGER.info("                Jacobian: %s%s",
-                                jac_method, num_method_str)
-                    minimizer_name = "{}: {}{}".format(
-                        minimizer, jac_method, num_method_str)
-
                 jacobian.method = num_method
                 cost_func.jacobian = jacobian
+                if minimizer_check:
+                    LOGGER.info(
+                        "                Jacobian: %s",
+                        jacobian.name() if jacobian.name() else "default"
+                    )
 
                 #######################
                 # Loops over Hessians #
                 #######################
-                new_result, new_minimizer_list = loop_over_hessians(
-                    controller, options, minimizer_name, grabbed_output)
+                new_result = loop_over_hessians(controller,
+                                                options,
+                                                grabbed_output)
 
-                minimizer_list.extend(new_minimizer_list)
                 results.extend(new_result)
                 # For minimizers that do not accept jacobians we raise an
                 # StopIteration exception to exit the loop through the
@@ -446,10 +410,10 @@ def loop_over_jacobians(controller, options, grabbed_output):
     except StopIteration:
         pass
 
-    return results, minimizer_list
+    return results
 
 
-def loop_over_hessians(controller, options, minimizer_name, grabbed_output):
+def loop_over_hessians(controller, options, grabbed_output):
     """
     Loops over Hessians set from the options file
 
@@ -457,15 +421,11 @@ def loop_over_hessians(controller, options, minimizer_name, grabbed_output):
     :type controller: Object derived from BaseSoftwareController
     :param options: FitBenchmarking options for current run
     :type options: fitbenchmarking.utils.options.Options
-    :param minimizer_name: minimizer name following loop_over_jacobians
-    :type minimizer_name: str
     :param grabbed_output: Object that removes third part output from console
     :type grabbed_output: fitbenchmarking.utils.output_grabber.OutputGrabber
 
-    :return: all results as a dictionary of arguments for FittingResult, and
-             composite minimizer-jacobian-hessian names.
+    :return: a FittingResult for each run
     :rtype: list[fibenchmarking.utils.fitbm_result.FittingResult],
-            list[str]
     """
     minimizer = controller.minimizer
     cost_func = controller.cost_func
@@ -473,76 +433,74 @@ def loop_over_hessians(controller, options, minimizer_name, grabbed_output):
     minimizer_check = minimizer in controller.hessian_enabled_solvers
     hessian_list = options.hes_method
     new_result = []
-    new_minimizer_list = []
 
     # loop over selected hessian methods
     for hes_method in hessian_list:
         # if user has selected to use hessian info
         # then create hessian if minimizer accepts it
-
-        minimizer_name_hes = minimizer_name
+        if minimizer_check and hes_method != 'default':
+            hessian_cls = create_hessian(hes_method)
+            try:
+                hessian = hessian_cls(cost_func.problem,
+                                      jacobian=cost_func.jacobian)
+                cost_func.hessian = hessian
+            except NoHessianError as excp:
+                LOGGER.warning(str(excp))
+                continue
+        else:
+            cost_func.hessian = None
 
         for num_method in options.hes_num_method[hes_method]:
-            if minimizer_check and hes_method != 'default':
-                hessian_cls = create_hessian(hes_method)
-                try:
-                    hessian = hessian_cls(cost_func.problem,
-                                          jacobian=cost_func.jacobian)
-                    cost_func.hessian = hessian
-
-                    num_method_str = ''
-                    if hes_method != "analytic":
-                        num_method_str = ' ' + num_method
-
-                    LOGGER.info("                   Hessian: %s%s",
-                                hes_method, num_method_str)
-                    minimizer_name_hes = "{}, {}{} hessian".format(
-                        minimizer_name, hes_method, num_method_str)
-
-                    hessian.method = num_method
-
-                except NoHessianError as excp:
-                    LOGGER.warning(str(excp))
-
-        # Perform the fit a number of times specified by num_runs
-        chi_sq, runtime = perform_fit(controller, options, grabbed_output)
-
-        # record algorithm type for specified minimizer
-        type_str = controller.record_alg_type(
-            minimizer, options.algorithm_type)
-        options.minimizer_alg_type[minimizer_name_hes] = type_str
-
-        result_args = {'options': options,
-                       'cost_func': cost_func,
-                       'jac': cost_func.jacobian,
-                       'hess': cost_func.hessian,
-                       'chi_sq': chi_sq,
-                       'runtime': runtime,
-                       'software': controller.software,
-                       'minimizer': minimizer_name_hes,
-                       'initial_params': controller.initial_params,
-                       'params': controller.final_params,
-                       'error_flag': controller.flag,
-                       'name': problem.name}
-        new_minimizer_list.append(minimizer_name_hes)
-        if problem.multifit:
-            # for multifit problems, multiple chi_sq values are stored
-            # in a list i.e. we have multiple results
-            for i in range(len(chi_sq)):
-                result_args.update(
-                    {'dataset_id': i,
-                     'name': f'{problem.name}, Dataset {i + 1}'})
+            if minimizer_check:
+                hess_name = "default"
+                if cost_func.hessian is not None:
+                    cost_func.hessian.method = num_method
+                    hess_name = cost_func.hessian.name()
+                LOGGER.info("                   Hessian: %s",
+                            hess_name)
+
+            # Perform the fit a number of times specified by num_runs
+            chi_sq, runtime = perform_fit(controller, options, grabbed_output)
+
+            jac_str = cost_func.jacobian.name() \
+                if minimizer in controller.jacobian_enabled_solvers else None
+            hess_str = cost_func.hessian.name() \
+                if cost_func.hessian is not None \
+                and minimizer in controller.hessian_enabled_solvers \
+                else None
+            result_args = {'options': options,
+                           'cost_func': cost_func,
+                           'jac': jac_str,
+                           'hess': hess_str,
+                           'chi_sq': chi_sq,
+                           'runtime': runtime,
+                           'software': controller.software,
+                           'minimizer': minimizer,
+                           'algorithm_type': controller.record_alg_type(
+                               minimizer, options.algorithm_type),
+                           'initial_params': controller.initial_params,
+                           'params': controller.final_params,
+                           'error_flag': controller.flag,
+                           'name': problem.name}
+            if problem.multifit:
+                # for multifit problems, multiple chi_sq values are stored
+                # in a list i.e. we have multiple results
+                for i in range(len(chi_sq)):
+                    result_args.update(
+                        {'dataset_id': i,
+                         'name': f'{problem.name}, Dataset {i + 1}'})
+                    new_result.append(
+                        fitbm_result.FittingResult(**result_args))
+            else:
                 new_result.append(fitbm_result.FittingResult(**result_args))
-        else:
-            new_result.append(fitbm_result.FittingResult(**result_args))
 
-        # For minimizers that do not accept hessians we raise an
-        # StopIteration exception to exit the loop through the
-        # Hessians
-        if not minimizer_check:
-            break
+            # For minimizers that do not accept hessians we raise an
+            # StopIteration exception to exit the loop through the
+            # Hessians
+            if not minimizer_check:
+                break
 
-    return new_result, new_minimizer_list
+    return new_result
 
 
 def perform_fit(controller, options, grabbed_output):
diff --git a/fitbenchmarking/core/results_output.py b/fitbenchmarking/core/results_output.py
index 268f6850..6075aafd 100644
--- a/fitbenchmarking/core/results_output.py
+++ b/fitbenchmarking/core/results_output.py
@@ -222,10 +222,10 @@ def _extract_tags(result: 'FittingResult', row_sorting: 'List[str]',
                        ('cat', cat_sorting)]:
         for sort_pos in order:
             if sort_pos in ['jacobian', 'hessian'] and result.error_flag == 4:
-                result_tags[tag] += ':.+'
+                result_tags[tag] += ':[^:]*'
             else:
                 result_tags[tag] += f':{getattr(result, sort_pos + "_tag")}'
-        result_tags[tag] = result_tags[tag].strip(':')
+        result_tags[tag] = result_tags[tag].lstrip(':')
 
     return result_tags
 
@@ -315,8 +315,7 @@ def create_plots(options, results, best_results, figures_dir):
             # If none of the fits succeeded, params could be None
             # Otherwise, add the best fit to the plot
             if best_in_cf.params is not None:
-                plot_path = plot.plot_best(best_in_cf.sanitised_min_name,
-                                           best_in_cf.params)
+                plot_path = plot.plot_best(best_in_cf)
                 best_in_cf.figure_link = plot_path
             else:
                 best_in_cf.figure_error = 'Minimizer failed to produce any ' \
@@ -335,8 +334,7 @@ def create_plots(options, results, best_results, figures_dir):
                 if not result.is_best_fit:
                     if result.params is not None:
                         cf = result.cost_func.__class__.__name__
-                        plot_path = plot_dict[cf].plot_fit(
-                            result.sanitised_min_name, result.params)
+                        plot_path = plot_dict[cf].plot_fit(result)
                         result.figure_link = plot_path
                     else:
                         result.figure_error = 'Minimizer failed to produce ' \
diff --git a/fitbenchmarking/hessian/analytic_hessian.py b/fitbenchmarking/hessian/analytic_hessian.py
index 078d1e15..d0e041c5 100644
--- a/fitbenchmarking/hessian/analytic_hessian.py
+++ b/fitbenchmarking/hessian/analytic_hessian.py
@@ -40,3 +40,12 @@ class Analytic(Hessian):
         """
         x = kwargs.get("x", self.problem.data_x)
         return self.problem.hessian(x, params)
+
+    def name(self) -> str:
+        """
+        Get a name for the current status of the jacobian.
+
+        :return: A unique name for this jacobian/method combination
+        :rtype: str
+        """
+        return "analytic"
diff --git a/fitbenchmarking/hessian/base_hessian.py b/fitbenchmarking/hessian/base_hessian.py
index 17526652..5807b4e4 100644
--- a/fitbenchmarking/hessian/base_hessian.py
+++ b/fitbenchmarking/hessian/base_hessian.py
@@ -61,3 +61,12 @@ class Hessian:
         :type value: str
         """
         self._method = value
+
+    def name(self) -> str:
+        """
+        Get a name for the current status of the hessian.
+
+        :return: A unique name for this hessian/method combination
+        :rtype: str
+        """
+        return f'{self.__class__.__name__.lower()} {self.method}'
diff --git a/fitbenchmarking/jacobian/analytic_jacobian.py b/fitbenchmarking/jacobian/analytic_jacobian.py
index 47576cb3..01e895e7 100644
--- a/fitbenchmarking/jacobian/analytic_jacobian.py
+++ b/fitbenchmarking/jacobian/analytic_jacobian.py
@@ -28,3 +28,12 @@ class Analytic(Jacobian):
         """
         x = kwargs.get("x", self.problem.data_x)
         return self.problem.jacobian(x, params)
+
+    def name(self) -> str:
+        """
+        Get a name for the current status of the jacobian.
+
+        :return: A unique name for this jacobian/method combination
+        :rtype: str
+        """
+        return "analytic"
diff --git a/fitbenchmarking/jacobian/base_jacobian.py b/fitbenchmarking/jacobian/base_jacobian.py
index eac2bfc4..e2bac6ac 100644
--- a/fitbenchmarking/jacobian/base_jacobian.py
+++ b/fitbenchmarking/jacobian/base_jacobian.py
@@ -59,3 +59,12 @@ class Jacobian:
         :type value: str
         """
         self._method = value
+
+    def name(self) -> str:
+        """
+        Get a name for the current status of the jacobian.
+
+        :return: A unique name for this jacobian/method combination
+        :rtype: str
+        """
+        return f'{self.__class__.__name__.lower()} {self.method}'
diff --git a/fitbenchmarking/jacobian/default_jacobian.py b/fitbenchmarking/jacobian/default_jacobian.py
index 84bf405d..5bd42717 100644
--- a/fitbenchmarking/jacobian/default_jacobian.py
+++ b/fitbenchmarking/jacobian/default_jacobian.py
@@ -25,3 +25,12 @@ class Default(Scipy):
         """
         self.method = "2-point"
         return super().eval(params, **kwargs)
+
+    def name(self) -> str:
+        """
+        Get a name for the current status of the jacobian.
+
+        :return: A unique name for this jacobian/method combination
+        :rtype: str
+        """
+        return ""
diff --git a/fitbenchmarking/results_processing/base_table.py b/fitbenchmarking/results_processing/base_table.py
index ca4d000d..10b3dbbf 100644
--- a/fitbenchmarking/results_processing/base_table.py
+++ b/fitbenchmarking/results_processing/base_table.py
@@ -289,7 +289,8 @@ class Table:
         """
         str_results = self.get_str_dict(html)
         row = next(iter(self.sorted_results.values()))
-        minimizers_list = [(r.software, r.minimizer) for r in row]
+        minimizers_list = [(r.software, r.modified_minimizer_name(False))
+                           for r in row]
         columns = pd.MultiIndex.from_tuples(minimizers_list)
         table = pd.DataFrame.from_dict(str_results,
                                        orient='index',
@@ -325,10 +326,9 @@ class Table:
         minimizers_list = [
             (cost_func_template.format(result.costfun_tag),
              software_template.format(result.software.replace('_', '-')),
-             minimizer_template.format(
-                 i, self.options.minimizer_alg_type[result.minimizer],
-                 result.software.replace('_', '-'),
-                 result.minimizer))
+             minimizer_template.format(i, result.algorithm_type,
+                                       result.software.replace('_', '-'),
+                                       result.modified_minimizer_name()))
             for i, result in enumerate(row)]
         columns = pd.MultiIndex.from_tuples(minimizers_list)
         table.columns = columns
diff --git a/fitbenchmarking/results_processing/fitting_report.py b/fitbenchmarking/results_processing/fitting_report.py
index 77377c6d..75621dfe 100644
--- a/fitbenchmarking/results_processing/fitting_report.py
+++ b/fitbenchmarking/results_processing/fitting_report.py
@@ -49,7 +49,7 @@ def create_prob_group(result, support_pages_dir, options):
     prob_name = result.sanitised_name
 
     file_name = f'{prob_name}_{result.costfun_tag}_' \
-                f'{result.sanitised_min_name}.html'
+                f'{result.sanitised_min_name(with_software=True)}.html'
     file_name = file_name.lower()
     file_path = os.path.join(support_pages_dir, file_name)
 
@@ -83,7 +83,7 @@ def create_prob_group(result, support_pages_dir, options):
             description=result.problem.description,
             equation=result.problem.equation,
             initial_guess=result.ini_function_params,
-            minimizer=result.minimizer,
+            minimizer=result.modified_minimizer_name(),
             accuracy=f"{result.chi_sq:.4g}",
             runtime=f"{result.runtime:.4g}",
             is_best_fit=result.is_best_fit,
diff --git a/fitbenchmarking/results_processing/performance_profiler.py b/fitbenchmarking/results_processing/performance_profiler.py
index 301030a8..93222ac0 100644
--- a/fitbenchmarking/results_processing/performance_profiler.py
+++ b/fitbenchmarking/results_processing/performance_profiler.py
@@ -47,16 +47,15 @@ def prepare_profile_data(results):
     for row in results.values():
         for cat in row.values():
             for i, result in enumerate(cat):
+                key = result.modified_minimizer_name(with_software=True)
                 if len(minimizers) <= i:
-                    minimizers.append(result.minimizer_tag)
-                    acc_dict[result.minimizer_tag] = []
-                    runtime_dict[result.minimizer_tag] = []
-                elif len(result.minimizer_tag) > len(minimizers[i]):
-                    acc_dict[result.minimizer_tag] = \
-                        acc_dict.pop(minimizers[i])
-                    runtime_dict[result.minimizer_tag] = \
-                        runtime_dict.pop(minimizers[i])
-                    minimizers[i] = result.minimizer_tag
+                    minimizers.append(key)
+                    acc_dict[key] = []
+                    runtime_dict[key] = []
+                elif len(key) > len(minimizers[i]):
+                    acc_dict[key] = acc_dict.pop(minimizers[i])
+                    runtime_dict[key] = runtime_dict.pop(minimizers[i])
+                    minimizers[i] = key
                 acc_dict[minimizers[i]].append(result.norm_acc)
                 runtime_dict[minimizers[i]].append(result.norm_runtime)
 
@@ -112,7 +111,8 @@ def plot(acc, runtime, fig_dir):
             legend_ax = 2
 
         # Plot linear performance profile
-        create_plot(ax[0], step_values, acc.keys())
+        keys = profile_plot.keys()
+        create_plot(ax[0], step_values, keys)
         ax[0].set_xlim(1, linear_upper_limit)
         ax[0].set_xticks([1, 2, 4, 6, 8, 10])
         ax[0].set_xticklabels(['$1$', '$2$', '$4$', '$6$', '$8$', '$10$'])
@@ -120,7 +120,7 @@ def plot(acc, runtime, fig_dir):
 
         if use_log_plot:
             # Plot log performance profile
-            create_plot(ax[1], step_values, acc.keys())
+            create_plot(ax[1], step_values, keys)
             ax[1].set_xlim(
                 linear_upper_limit,
                 min(max_value+1, 10000))
diff --git a/fitbenchmarking/results_processing/plots.py b/fitbenchmarking/results_processing/plots.py
index f1a53bd7..558bd7a6 100644
--- a/fitbenchmarking/results_processing/plots.py
+++ b/fitbenchmarking/results_processing/plots.py
@@ -180,20 +180,18 @@ class Plot:
         self.fig.savefig(file_name)
         return file
 
-    def plot_best(self, minimizer, params):
+    def plot_best(self, result):
         """
         Plots the fit along with the data using the "best_fit" style
         and saves to a file
 
-        :param minimizer: name of the best fit minimizer
-        :type minimizer: str
-        :param params: fit parameters returned from the best fit minimizer
-        :type params: list
+        :param result: The result to plot
+        :type result: FittingResult
 
         :return: path to the saved file
         :rtype: str
         """
-        label = 'Best Fit ({})'.format(minimizer)
+        label = f'Best Fit ({result.modified_minimizer_name(True)})'
 
         # Plot line and save.
         # This should have the style of fit plot options and the colour of
@@ -204,13 +202,13 @@ class Plot:
         plot_options_dict['label'] = label
         plot_options_dict['color'] = self.best_fit_plot_options['color']
 
-        y = self.problem.eval_model(params, x=self.x)
+        y = self.problem.eval_model(result.params, x=self.x)
         self.plot_data(errors=False,
                        plot_options=plot_options_dict,
                        y=y)
         self.format_plot()
-        file = f"{minimizer}_fit_for_{self.result.costfun_tag}_" \
-               f"{self.result.sanitised_name}.png"
+        file = f"{result.sanitised_min_name(True)}_fit_for_" \
+               f"{self.result.costfun_tag}_{self.result.sanitised_name}.png"
         file_name = os.path.join(self.figures_dir, file)
         self.fig.savefig(file_name)
 
@@ -226,30 +224,28 @@ class Plot:
         self.line_plot = None
         return file
 
-    def plot_fit(self, minimizer, params):
+    def plot_fit(self, result):
         """
         Updates self.line to show the fit using the passed in params.
         If self.line is empty it will create a new line.
         Stores the plot in a file
 
-        :param minimizer: name of the fit minimizer
-        :type minimizer: str
-        :param params: fit parameters returned from the best fit minimizer
-        :type params: list
+        :param result: The result to plot
+        :type result: FittingResult
 
         :return: path to the saved file
         :rtype: str
         """
         plot_options_dict = self.fit_plot_options.copy()
-        plot_options_dict['label'] = minimizer
+        plot_options_dict['label'] = result.modified_minimizer_name(True)
 
         self.plot_data(errors=False,
                        plot_options=plot_options_dict,
                        x=self.x,
-                       y=self.problem.eval_model(params, x=self.x))
+                       y=self.problem.eval_model(result.params, x=self.x))
         self.format_plot()
-        file = f"{minimizer}_fit_for_{self.result.costfun_tag}_"\
-               f"{self.result.sanitised_name}.png"
+        file = f"{result.sanitised_min_name(True)}_fit_for_"\
+               f"{self.result.costfun_tag}_{self.result.sanitised_name}.png"
         file_name = os.path.join(self.figures_dir, file)
         self.fig.savefig(file_name)
         return file
diff --git a/fitbenchmarking/templates/problem_summary_page_template.html b/fitbenchmarking/templates/problem_summary_page_template.html
index 6577d0e7..df77db8d 100644
--- a/fitbenchmarking/templates/problem_summary_page_template.html
+++ b/fitbenchmarking/templates/problem_summary_page_template.html
@@ -90,12 +90,12 @@
                 <section class="accordion">
                     <input type="checkbox" name="collapse" id="best-accordion">
                     <h2 class="handle">
-                        <label for="best-accordion">{{ categories[0] }}: {{ best_results[0].minimizer }}</label>
+                        <label for="best-accordion">{{ categories[0] }}: {{ best_results[0].modified_minimizer_name(True) }}</label>
                     </h2>
                     <div class="content">
                         <p>This is the best fit of the minimizers used.</p>
 
-                        <p><em>Minimizer</em>: {{ best_results[0].minimizer }}</p>
+                        <p><em>Minimizer</em>: {{ best_results[0].modified_minimizer_name(True) }}</p>
                         <p><em>Functions</em>:</p>
                         <table>
                             <colgroup>
@@ -130,12 +130,12 @@
                         <input type="checkbox" name="collapse" id="cf{{ loop.index0 }}-accordion">
                         <h2 class="handle">
                             <label for="cf{{ loop.index0 }}-accordion">{{ categories[loop.index0] }}:
-                                {{ best_results[loop.index0].minimizer }}</label>
+                                {{ best_results[loop.index0].modified_minimizer_name(True) }}</label>
                         </h2>
                         <div class="content">
                             <p>{{ plot_descriptions[loop.index0] }}</p>
 
-                            <p><em>Minimizer</em>: {{ best_results[loop.index0].minimizer }}</p>
+                            <p><em>Minimizer</em>: {{ best_results[loop.index0].modified_minimizer_name(True) }}</p>
                             <p><em>Functions</em>:</p>
                             <table>
                                 <colgroup>
diff --git a/fitbenchmarking/utils/fitbm_result.py b/fitbenchmarking/utils/fitbm_result.py
index 4b4f7e56..7040cb9f 100644
--- a/fitbenchmarking/utils/fitbm_result.py
+++ b/fitbenchmarking/utils/fitbm_result.py
@@ -16,7 +16,8 @@ class FittingResult:
 
     def __init__(self, options, cost_func, jac, hess, initial_params, params,
                  name=None, chi_sq=None, runtime=None, software=None,
-                 minimizer=None, error_flag=None, dataset_id=None):
+                 minimizer=None, error_flag=None, algorithm_type=None,
+                 dataset_id=None):
         """
         Initialise the Fitting Result
 
@@ -25,10 +26,10 @@ class FittingResult:
         :param cost_func: Cost function object selected from options.
         :type cost_func: subclass of
                 :class:`~fitbenchmarking.cost_func.base_cost_func.CostFunc`
-        :param jac: The Jacobian definition
-        :type jac: fitbenchmarking.jacobian.base_jacobian.Jacobian subclass
-        :param hess: The Hessian definition
-        :type hess: fitbenchmarking.hessian.base_hessian.Hessian subclass
+        :param jac: The Jacobian used in the fitting
+        :type jac: str
+        :param hess: The Hessian used in the fitting
+        :type hess: str
         :param initial_params: The starting parameters for the fit
         :type initial_params: list of float
         :param params: The parameters found by the fit
@@ -45,6 +46,9 @@ class FittingResult:
         :type minimizer: str, optional
         :param error_flag: [description], defaults to None
         :type error_flag: [type], optional
+        :param algorithm_type: The tags associated with the minimizer,
+                               defaults to None
+        :type algorithm_type: str, optional
         :param dataset_id: The index of the dataset (Only used for MultiFit),
                            defaults to None
         :type dataset_id: int, optional
@@ -52,8 +56,6 @@ class FittingResult:
         self.options = options
         self.cost_func = cost_func
         self.problem = self.cost_func.problem
-        self.jac = jac
-        self.hess = hess
         self.name = name if name is not None else \
             self.problem.name
 
@@ -84,6 +86,9 @@ class FittingResult:
         # Minimizer for a certain problem and its function definition
         self.software = software
         self.minimizer = minimizer
+        self.algorithm_type = algorithm_type
+        self.jac = jac
+        self.hess = hess
 
         # String interpretations of the params
         self.ini_function_params = self.problem.get_function_params(
@@ -109,12 +114,14 @@ class FittingResult:
         self.is_best_fit = False
 
         # Attributes for table creation
-        self.costfun_tag = self.cost_func.__class__.__name__
-        self.problem_tag = self.name
-        self.software_tag = self.software
-        self.minimizer_tag = self.minimizer
-        self.jacobian_tag = self.jac.__class__.__name__
-        self.hessian_tag = self.hess.__class__.__name__
+        self.costfun_tag: str = self.cost_func.__class__.__name__
+        self.problem_tag: str = self.name
+        self.software_tag: str = self.software \
+            if self.software is not None else ""
+        self.minimizer_tag: str = self.minimizer \
+            if self.minimizer is not None else ""
+        self.jacobian_tag: str = self.jac if self.jac is not None else ""
+        self.hessian_tag: str = self.hess if self.hess is not None else ""
 
     def __str__(self):
         info = {"Cost Function": self.costfun_tag,
@@ -128,6 +135,39 @@ class FittingResult:
 
         return get_printable_table("FittingResult", info)
 
+    def modified_minimizer_name(self, with_software: bool = False) -> str:
+        """
+        Get a minimizer name which contains jacobian and hessian information.
+        Optionally also include the software.
+
+        :param with_software: Add software to the name, defaults to False
+        :type with_software: bool, optional
+        :return: A name for the result combination
+        :rtype: str
+        """
+        name: str = self.minimizer_tag
+        if with_software:
+            name += f' [{self.software_tag}]'
+
+        if self.jacobian_tag:
+            name += f': j:{self.jacobian_tag}'
+
+        if self.hessian_tag:
+            name += f' h:{self.hessian_tag}'
+
+        return name
+
+    def sanitised_min_name(self, with_software=False):
+        """
+        Sanitise the modified minimizer name into one which can be used as a
+        filename.
+
+        :return: sanitised name
+        :rtype: str
+        """
+        return self.modified_minimizer_name(with_software)\
+            .replace(':', '').replace(' ', '_')
+
     @property
     def norm_acc(self):
         """
@@ -188,20 +228,6 @@ class FittingResult:
         """
         return self.name.replace(',', '').replace(' ', '_')
 
-    @property
-    def sanitised_min_name(self):
-        """
-        Sanitise the minimizer name into one which can be used as a filename.
-
-        :return: sanitised name
-        :rtype: str
-        """
-        return self.minimizer.replace(':', '').replace(' ', '_')
-
     @sanitised_name.setter
     def sanitised_name(self, value):
         raise RuntimeError('sanitised_name can not be edited')
-
-    @sanitised_min_name.setter
-    def sanitised_min_name(self, value):
-        raise RuntimeError('sanitised_min_name can not be edited')


Test patch:
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark.py
index 1733f268..07e0bad6 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark.py
@@ -88,7 +88,6 @@ class BenchmarkTests(unittest.TestCase):
 
         assert failed_problems == []
         dict_test(expected_unselected_minimizers, unselected_minimizers)
-        dict_test(expected_minimizers, self.options.minimizers)
 
     @mock.patch('{}.loop_over_benchmark_problems'.format(FITTING_DIR))
     def test_check_no_unselected_minimizers(self,
@@ -116,8 +115,7 @@ class BenchmarkTests(unittest.TestCase):
         expected_minimizers = copy.copy(self.all_minimizers)
         expected_unselected_minimizers = {"scipy": []}
         loop_over_benchmark_problems.return_value = \
-            (results, problem_fails, expected_unselected_minimizers,
-             expected_minimizers)
+            (results, problem_fails, expected_unselected_minimizers)
 
         # run shared test and see if it match expected
         self.shared_tests(expected_problem_names,
@@ -153,8 +151,7 @@ class BenchmarkTests(unittest.TestCase):
         expected_minimizers = copy.copy(self.all_minimizers)
 
         loop_over_benchmark_problems.return_value = \
-            (results, problem_fails, expected_unselected_minimizers,
-             expected_minimizers)
+            (results, problem_fails, expected_unselected_minimizers)
 
         self.shared_tests(expected_names, expected_unselected_minimizers,
                           expected_minimizers)
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark_problems.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark_problems.py
index 6f10ad35..640cbf47 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark_problems.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_benchmark_problems.py
@@ -92,9 +92,8 @@ class LoopOverBenchmarkProblemsTests(unittest.TestCase):
         problem_fails = self.problem_fails
         unselected_minimizers = {"scipy": []}
         self.count += 1
-        expect_minimizers = {"scipy": ['TNC', 'lm-scipy']}
         return individual_problem_results, problem_fails, \
-            unselected_minimizers, expect_minimizers
+            unselected_minimizers
 
     def shared_tests(self, list_len, expected_problem_fails):
         """
@@ -105,12 +104,11 @@ class LoopOverBenchmarkProblemsTests(unittest.TestCase):
         :param expected_problem_fails: list of problems which fail
         :type expected_problem_fails: list
         """
-        results, failed_problems, unselected_minimizers, minimizer_dict = \
+        results, failed_problems, unselected_minimizers = \
             loop_over_benchmark_problems(self.problem_group, self.options)
         assert len(results) == list_len
         assert failed_problems == expected_problem_fails
         dict_test(unselected_minimizers, {"scipy": []})
-        dict_test(minimizer_dict, {"scipy": ['TNC', 'lm-scipy']})
 
     @unittest.mock.patch('{}.loop_over_starting_values'.format(FITTING_DIR))
     def test_run_multiple_benchmark_problems(self, loop_over_starting_values):
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_hessians.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_hessians.py
index 98a64c73..376248a1 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_hessians.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_hessians.py
@@ -6,10 +6,14 @@ import os
 import unittest
 from unittest.mock import patch
 
+from pytest import test_type as TEST_TYPE  # pylint: disable=no-name-in-module
+
+from conftest import run_for_test_types
 from fitbenchmarking import mock_problems
 from fitbenchmarking.controllers.base_controller import Controller
 from fitbenchmarking.core.fitting_benchmarking import loop_over_hessians
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
+from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.parsing.parser_factory import parse_problem_file
 from fitbenchmarking.utils import output_grabber
 from fitbenchmarking.utils.options import Options
@@ -102,6 +106,7 @@ class LoopOverHessiansTests(unittest.TestCase):
         self.minimizers = ["deriv_free_algorithm", "general"]
         self.cost_func = make_cost_function(minimizers=self.minimizers)
         self.problem = self.cost_func.problem
+        self.cost_func.jacobian = Scipy(self.problem)
         self.controller = DummyController(cost_func=self.cost_func)
         self.options = self.problem.options
         self.grabbed_output = output_grabber.OutputGrabber(self.options)
@@ -113,30 +118,10 @@ class LoopOverHessiansTests(unittest.TestCase):
         """
         self.options.hes_method = ["analytic"]
         self.controller.minimizer = "general"
-        new_name = ['general, analytic hessian']
-        minimizer_name = "general"
-        _, new_minimizer_list = \
-            loop_over_hessians(self.controller,
-                               self.options,
-                               minimizer_name,
-                               self.grabbed_output)
-        assert new_minimizer_list == new_name
-
-    def test_single_no_hessian(self):
-        """
-        Test that checks that the minimizer doesn't need Hessian information
-        and the name does not have Hessian information in it
-        """
-        self.options.hes_method = ["analytic"]
-        self.controller.minimizer = "deriv_free_algorithm"
-        new_name = ['deriv_free_algorithm']
-        minimizer_name = "deriv_free_algorithm"
-        _, new_minimizer_list = \
-            loop_over_hessians(self.controller,
+        _ = loop_over_hessians(self.controller,
                                self.options,
-                               minimizer_name,
                                self.grabbed_output)
-        assert new_minimizer_list == new_name
+        self.assertEqual(self.controller.count, 1)
 
     @patch.object(DummyController, "check_bounds_respected")
     def test_bounds_respected_func_called(
@@ -148,11 +133,9 @@ class LoopOverHessiansTests(unittest.TestCase):
         """
         self.controller.problem.value_ranges = {'test': (0, 1)}
         self.controller.minimizer = "deriv_free_algorithm"
-        minimizer_name = "deriv_free_algorithm"
 
         _ = loop_over_hessians(self.controller,
                                self.options,
-                               minimizer_name,
                                self.grabbed_output)
         check_bounds_respected.assert_called()
 
@@ -166,11 +149,9 @@ class LoopOverHessiansTests(unittest.TestCase):
         self.controller.problem.value_ranges = {'test': (0, 1)}
         self.controller.minimizer = "deriv_free_algorithm"
         self.controller.flag_expected = [3]
-        minimizer_name = "deriv_free_algorithm"
 
         _ = loop_over_hessians(self.controller,
                                self.options,
-                               minimizer_name,
                                self.grabbed_output)
         check_bounds_respected.assert_not_called()
 
@@ -179,20 +160,39 @@ class LoopOverHessiansTests(unittest.TestCase):
         """
         Test that the correct flag is set when the max_runtime is exceeded.
         """
-        self.cost_func = make_cost_function(minimizers=self.minimizers,
-                                            max_runtime=0.1)
-        self.cost_func.problem.timer.total_elapsed_time = 5
-        self.controller = DummyController(cost_func=self.cost_func)
-        self.options = self.cost_func.problem.options
-        self.grabbed_output = output_grabber.OutputGrabber(self.options)
-        self.controller.parameter_set = 0
-
-        self.controller.minimizer = "deriv_free_algorithm"
-        results, _ = loop_over_hessians(self.controller, self.options,
-                                        self.controller.minimizer,
-                                        self.grabbed_output)
+        cost_func = make_cost_function(minimizers=self.minimizers,
+                                       max_runtime=0.1)
+        cost_func.jacobian = Scipy(cost_func.problem)
+        cost_func.problem.timer.total_elapsed_time = 5
+        controller = DummyController(cost_func=cost_func)
+        options = cost_func.problem.options
+        grabbed_output = output_grabber.OutputGrabber(options)
+        controller.parameter_set = 0
+
+        controller.minimizer = "deriv_free_algorithm"
+        results = loop_over_hessians(controller, options,
+                                     grabbed_output)
         self.assertEqual(results[0].error_flag, 6)
 
+    @run_for_test_types(TEST_TYPE, 'all')
+    @patch('fitbenchmarking.core.fitting_benchmarking.perform_fit')
+    def test_multifit_num_results(self, perform_fit):
+        """
+        Test that a multifit problem produces the correct number of results.
+        """
+        cost_func = make_cost_function('multifit_set/multifit.txt')
+        problem = cost_func.problem
+        cost_func.jacobian = Scipy(problem)
+        controller = DummyController(cost_func=cost_func)
+        options = problem.options
+        grabbed_output = output_grabber.OutputGrabber(options)
+        controller.final_params = [[0.1, 0.1], [0.1, 0.1]]
+        perform_fit.return_value = ([0.1, 0.2], [0.1, 0.01])
+        results = loop_over_hessians(controller=controller,
+                                     options=options,
+                                     grabbed_output=grabbed_output)
+        self.assertTrue(len(results) == 2)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_jacobians.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_jacobians.py
index 61c5e451..29353e40 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_jacobians.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_jacobians.py
@@ -104,8 +104,7 @@ class LoopOverJacobiansTests(unittest.TestCase):
         """
         Mock function to be used instead of loop_over_hessians
         """
-        minimizer_name = args[2]
-        return [], [minimizer_name]
+        return []
 
     @patch('{}.loop_over_hessians'.format(FITTING_DIR))
     def test_single_jacobian(self, loop_over_hessians):
@@ -116,12 +115,10 @@ class LoopOverJacobiansTests(unittest.TestCase):
         self.options.jac_num_method = {"scipy": ["3-point"]}
         self.controller.minimizer = "general"
         loop_over_hessians.side_effect = self.mock_func_call
-        new_name = ['general: scipy 3-point']
-        _, new_minimizer_list = \
-            loop_over_jacobians(self.controller,
+        _ = loop_over_jacobians(self.controller,
                                 self.options,
                                 self.grabbed_output)
-        assert new_minimizer_list == new_name
+        loop_over_hessians.assert_called_once()
 
     @patch('{}.loop_over_hessians'.format(FITTING_DIR))
     def test_multiple_jacobian(self, loop_over_hessians):
@@ -132,29 +129,10 @@ class LoopOverJacobiansTests(unittest.TestCase):
         self.options.jac_num_method = {"scipy": ["3-point", "2-point"]}
         self.controller.minimizer = "general"
         loop_over_hessians.side_effect = self.mock_func_call
-        new_name = ['general: scipy 3-point', 'general: scipy 2-point']
-        _, new_minimizer_list = \
-            loop_over_jacobians(self.controller,
+        _ = loop_over_jacobians(self.controller,
                                 self.options,
                                 self.grabbed_output)
-        assert new_minimizer_list == new_name
-
-    @patch('{}.loop_over_hessians'.format(FITTING_DIR))
-    def test_single_no_jacobian(self, loop_over_hessians):
-        """
-        Test that checks that the minimizer doesn't need Jacobian information
-        and the name does not have Jacobian information in it
-        """
-        self.options.jac_method = ["scipy"]
-        self.options.jac_num_method = {"scipy": ["3-point", "2-point"]}
-        self.controller.minimizer = "deriv_free_algorithm"
-        loop_over_hessians.side_effect = self.mock_func_call
-        new_name = ['deriv_free_algorithm']
-        _, new_minimizer_list = \
-            loop_over_jacobians(self.controller,
-                                self.options,
-                                self.grabbed_output)
-        assert new_minimizer_list == new_name
+        self.assertEqual(loop_over_hessians.call_count, 2)
 
 
 if __name__ == "__main__":
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_minimizers.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_minimizers.py
index bacddab4..aa48fd8e 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_minimizers.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_minimizers.py
@@ -108,21 +108,19 @@ class LoopOverMinimizersTests(unittest.TestCase):
         Mock function to be used instead of loop_over_jacobians
         """
         results = self.results[self.count]
-        minimizer_list = self.minimizer_list[self.count]
         self.count += 1
-        return results, minimizer_list
+        return results
 
     def test_run_minimzers_none_selected(self):
         """
         Tests that no minimizers are selected
         """
         self.options.algorithm_type = ["ls"]
-        results_problem, minimizer_failed, new_minimizer_list = \
+        results_problem, minimizer_failed = \
             loop_over_minimizers(self.controller, self.minimizers,
                                  self.options, self.grabbed_output)
         assert results_problem == []
         assert minimizer_failed == self.minimizers
-        assert new_minimizer_list == []
 
     @patch('{}.loop_over_jacobians'.format(FITTING_DIR))
     def test_run_minimzers_selected(self, loop_over_hessians):
@@ -134,13 +132,12 @@ class LoopOverMinimizersTests(unittest.TestCase):
         self.minimizer_list = [["general"]]
         loop_over_hessians.side_effect = self.mock_func_call
 
-        results_problem, minimizer_failed, new_minimizer_list = \
+        results_problem, minimizer_failed = \
             loop_over_minimizers(self.controller, self.minimizers,
                                  self.options, self.grabbed_output)
         assert all(isinstance(x, fitbm_result.FittingResult)
                    for x in results_problem)
         assert minimizer_failed == ["deriv_free_algorithm"]
-        assert new_minimizer_list == ["general"]
 
     @patch('{}.loop_over_jacobians'.format(FITTING_DIR))
     def test_run_minimzers_all(self, loop_over_hessians):
@@ -151,13 +148,12 @@ class LoopOverMinimizersTests(unittest.TestCase):
         self.minimizer_list = [["general"], ["deriv_free_algorithm"]]
         loop_over_hessians.side_effect = self.mock_func_call
 
-        results_problem, minimizer_failed, new_minimizer_list = \
+        results_problem, minimizer_failed = \
             loop_over_minimizers(self.controller, self.minimizers,
                                  self.options, self.grabbed_output)
         assert all(isinstance(x, fitbm_result.FittingResult)
                    for x in results_problem)
         assert minimizer_failed == []
-        assert new_minimizer_list == ["general", "deriv_free_algorithm"]
 
     def test_no_bounds_minimizer(self):
         """
@@ -168,13 +164,12 @@ class LoopOverMinimizersTests(unittest.TestCase):
         self.controller.problem.value_ranges = {'test': (0, 1)}
         self.minimizers = ["general"]
 
-        results_problem, minimizer_failed, new_minimizer_list = \
+        results_problem, minimizer_failed = \
             loop_over_minimizers(self.controller, self.minimizers,
                                  self.options, self.grabbed_output)
 
         assert results_problem[0].error_flag == 4
         assert minimizer_failed == []
-        assert new_minimizer_list == ["general"]
 
 
 if __name__ == "__main__":
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_software.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_software.py
index 3b085222..78e64027 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_software.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_software.py
@@ -86,12 +86,10 @@ class LoopOverSoftwareTests(unittest.TestCase):
         """
         minimizer_failed = list(self.minimizer_failed.values())[self.count]
         results_problem = self.results_problem[self.count]
-        minimizer_success = list(self.minimizer_success.values())[self.count]
         self.count += 1
-        return results_problem, minimizer_failed, minimizer_success
+        return results_problem, minimizer_failed
 
-    def shared_test(self, expected_list_len, expected_minimizer_failed,
-                    expected_minimizer_dict):
+    def shared_test(self, expected_list_len, expected_minimizer_failed):
         """
         Shared tests for the `loop_over_fitting_software` function
 
@@ -99,10 +97,8 @@ class LoopOverSoftwareTests(unittest.TestCase):
         :type expected_list_len: int
         :param expected_minimizer_failed: expected dict of failed minimizers
         :type expected_minimizer_failed: dict
-        :param expected_minimizer_dict: expected dict of successful minimizers
-        :type expected_minimizer_dict: dict
         """
-        results, unselected_minimzers, minimizer_dict = \
+        results, unselected_minimzers = \
             loop_over_fitting_software(self.cost_func,
                                        self.options,
                                        self.start_values_index,
@@ -110,7 +106,6 @@ class LoopOverSoftwareTests(unittest.TestCase):
         assert len(results) == expected_list_len
 
         dict_test(unselected_minimzers, expected_minimizer_failed)
-        dict_test(minimizer_dict, expected_minimizer_dict)
 
     @unittest.mock.patch('{}.loop_over_minimizers'.format(FITTING_DIR))
     def test_run_software(self, loop_over_minimizers):
@@ -120,7 +115,6 @@ class LoopOverSoftwareTests(unittest.TestCase):
         """
         self.count = 0
         self.minimizer_failed = {'scipy': [], 'dfo': []}
-        self.minimizer_success = {'scipy': ['TNC'], 'dfo': ['dfogn']}
         self.results_problem = \
             [[fitbm_result.FittingResult(**self.result_args)
               for i in range(self.scipy_len)],
@@ -129,9 +123,7 @@ class LoopOverSoftwareTests(unittest.TestCase):
         loop_over_minimizers.side_effect = self.mock_func_call
         expected_list_len = self.scipy_len + self.dfo_len
         expected_minimizer_failed = self.minimizer_failed
-        expected_minimizer_success = self.minimizer_success
-        self.shared_test(expected_list_len, expected_minimizer_failed,
-                         expected_minimizer_success)
+        self.shared_test(expected_list_len, expected_minimizer_failed)
 
     @unittest.mock.patch('{}.loop_over_minimizers'.format(FITTING_DIR))
     def test_run_software_failed_minimizers(self, loop_over_minimizers):
@@ -141,7 +133,6 @@ class LoopOverSoftwareTests(unittest.TestCase):
         self.count = 0
         self.minimizer_failed = {'scipy': ['Powell'],
                                  'dfo': ['dfogn', 'dfols']}
-        self.minimizer_success = {'scipy': ['TNC', 'CG'], 'dfo': ['dfogn']}
         failed_scipy = 1
         failed_dfo = 2
         self.results_problem = \
@@ -153,9 +144,7 @@ class LoopOverSoftwareTests(unittest.TestCase):
         expected_list_len = self.scipy_len + self.dfo_len - \
             failed_scipy - failed_dfo
         expected_minimizer_failed = self.minimizer_failed
-        expected_minimizer_success = self.minimizer_success
-        self.shared_test(expected_list_len, expected_minimizer_failed,
-                         expected_minimizer_success)
+        self.shared_test(expected_list_len, expected_minimizer_failed)
 
     @unittest.mock.patch('{}.loop_over_minimizers'.format(FITTING_DIR))
     def test_run_software_all_failed_minimizers(self, loop_over_minimizers):
@@ -166,7 +155,6 @@ class LoopOverSoftwareTests(unittest.TestCase):
         self.count = 0
         self.minimizer_failed = {s: self.options.minimizers[s]
                                  for s in self.options.software}
-        self.minimizer_success = {'scipy': ['TNC', 'CG'], 'dfo': ['dfogn']}
 
         self.result_args['chi_sq'] = np.inf
         self.results_problem = [[fitbm_result.FittingResult(**self.result_args)
@@ -176,9 +164,7 @@ class LoopOverSoftwareTests(unittest.TestCase):
         loop_over_minimizers.side_effect = self.mock_func_call
         expected_list_len = 10
         expected_minimizer_failed = self.minimizer_failed
-        expected_minimizer_success = self.minimizer_success
-        self.shared_test(expected_list_len, expected_minimizer_failed,
-                         expected_minimizer_success)
+        self.shared_test(expected_list_len, expected_minimizer_failed)
 
     def test_incorrect_software(self):
         """
diff --git a/fitbenchmarking/core/tests/test_fitting_benchmarking_starting_values.py b/fitbenchmarking/core/tests/test_fitting_benchmarking_starting_values.py
index ab10390f..1240e728 100644
--- a/fitbenchmarking/core/tests/test_fitting_benchmarking_starting_values.py
+++ b/fitbenchmarking/core/tests/test_fitting_benchmarking_starting_values.py
@@ -85,10 +85,8 @@ class LoopOverStartingValuesTests(unittest.TestCase):
             self.individual_problem_results[self.count]
         unselected_minimizers = self.unselected_minimizers
         self.count += 1
-        expect_minimizers = {"scipy": ['TNC', 'lm-scipy']}
         return (individual_problem_results,
-                unselected_minimizers,
-                expect_minimizers)
+                unselected_minimizers)
 
     def shared_tests(self, expected_list_len, expected_problem_fails,
                      expected_unselected_minimizers):
@@ -103,15 +101,14 @@ class LoopOverStartingValuesTests(unittest.TestCase):
                                                unselected minimizer
         :type expected_unselected_minimizers: dict
         """
-        problem_results, problem_fails, unselected_minimizers,\
-            minimizer_dict = loop_over_starting_values(self.problem,
-                                                       self.options,
-                                                       self.grabbed_output)
+        problem_results, problem_fails, unselected_minimizers \
+            = loop_over_starting_values(self.problem,
+                                        self.options,
+                                        self.grabbed_output)
         assert len(problem_results) == expected_list_len
         assert problem_fails == expected_problem_fails
 
         dict_test(unselected_minimizers, expected_unselected_minimizers)
-        dict_test(minimizer_dict, {"scipy": ['TNC', 'lm-scipy']})
 
     @unittest.mock.patch('{}.loop_over_fitting_software'.format(FITTING_DIR))
     def test_run_multiple_starting_values(self, loop_over_fitting_software):
diff --git a/fitbenchmarking/core/tests/test_results_output.py b/fitbenchmarking/core/tests/test_results_output.py
index fa83077d..0461848d 100644
--- a/fitbenchmarking/core/tests/test_results_output.py
+++ b/fitbenchmarking/core/tests/test_results_output.py
@@ -22,8 +22,6 @@ from fitbenchmarking.core.results_output import (_extract_tags,
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
 from fitbenchmarking.cost_func.weighted_nlls_cost_func import \
     WeightedNLLSCostFunc
-from fitbenchmarking.jacobian.numdifftools_jacobian import Numdifftools
-from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.parsing.fitting_problem import FittingProblem
 from fitbenchmarking.utils.exceptions import PlottingError
 from fitbenchmarking.utils.fitbm_result import FittingResult
@@ -92,8 +90,7 @@ def generate_mock_results(results_directory: str):
 
     softwares = ['s1', 's2']
     minimizers = [['s1m1', 's1m2'], ['s2m1', 's2m2']]
-    jacobians = [[Scipy(p), Numdifftools(p)]
-                 for p in problems]
+    jacobians = [['j1', 'j2'] for _ in problems]
     cost_funcs = [[NLLSCostFunc(p), WeightedNLLSCostFunc(p)]
                   for p in problems]
 
@@ -471,8 +468,8 @@ class ExtractTagsTests(unittest.TestCase):
                              col_sorting=['hessian'],
                              cat_sorting=['costfun', 'software', 'minimizer'])
 
-        self.assertDictEqual(tags, {'row': '.+:p0',
-                                    'col': '.+',
+        self.assertDictEqual(tags, {'row': '[^:]*:p0',
+                                    'col': '[^:]*',
                                     'cat': 'cf0:s0:m0'})
 
 
@@ -485,14 +482,14 @@ class FindMatchingTagsTests(unittest.TestCase):
         """
         Test that the matching tags include all correct tags.
         """
-        matching = _find_matching_tags('cf0:.+:p0', ['cf0:j0:p0',
-                                                     'cf0:j0:p1',
-                                                     'cf0:j1:p0',
-                                                     'cf0:j1:p1',
-                                                     'cf1:j0:p0',
-                                                     'cf1:j0:p1',
-                                                     'cf1:j1:p0',
-                                                     'cf1:j1:p1'])
+        matching = _find_matching_tags('cf0:[^:]*:p0', ['cf0:j0:p0',
+                                                        'cf0:j0:p1',
+                                                        'cf0:j1:p0',
+                                                        'cf0:j1:p1',
+                                                        'cf1:j0:p0',
+                                                        'cf1:j0:p1',
+                                                        'cf1:j1:p0',
+                                                        'cf1:j1:p1'])
         self.assertIn('cf0:j0:p0', matching)
         self.assertIn('cf0:j1:p0', matching)
 
@@ -500,10 +497,10 @@ class FindMatchingTagsTests(unittest.TestCase):
         """
         Test that all tags that don't match are excluded.
         """
-        matching = _find_matching_tags('cf0:.+:p0', ['cf0:j0:p0',
-                                                     'cf0:j1:p0',
-                                                     'cf1:j0:p0',
-                                                     'cf1:j1:p0'])
+        matching = _find_matching_tags('cf0:[^:]*:p0', ['cf0:j0:p0',
+                                                        'cf0:j1:p0',
+                                                        'cf1:j0:p0',
+                                                        'cf1:j1:p0'])
         self.assertNotIn('cf1:j0:p0', matching)
         self.assertNotIn('cf1:j1:p0', matching)
 
diff --git a/fitbenchmarking/hessian/tests/test_hessian.py b/fitbenchmarking/hessian/tests/test_hessian.py
index 26b7bdfa..c915ce6f 100644
--- a/fitbenchmarking/hessian/tests/test_hessian.py
+++ b/fitbenchmarking/hessian/tests/test_hessian.py
@@ -111,6 +111,41 @@ def grad2_r_poisson(x, y, p):
                       p[0]*x**2*np.exp((x*p[1]))], ])
 
 
+class TestHessianName(TestCase):
+    """
+    Tests for the name function
+    """
+
+    def setUp(self):
+        """
+        Setting up tests
+        """
+        options = Options()
+        self.fitting_problem = FittingProblem(options)
+        self.fitting_problem.function = f_ls
+        self.fitting_problem.jacobian = J_ls
+        self.fitting_problem.hessian = H_ls
+        self.fitting_problem.data_x = np.array([1, 2, 3, 4, 5])
+        self.fitting_problem.data_y = np.array([1, 2, 4, 8, 16])
+        self.jacobian = JacobianClass(self.fitting_problem)
+
+    def test_scipy_hessian(self):
+        """
+        Test the name is correct for the scipy hessian.
+        """
+        hessian = Scipy(self.fitting_problem, self.jacobian)
+        hessian.method = 'some_method'
+        self.assertEqual(hessian.name(), "scipy some_method")
+
+    def test_analytic_hessian(self):
+        """
+        Test the name is correct for the analytic hessian.
+        """
+        hessian = Analytic(self.fitting_problem, self.jacobian)
+        hessian.method = 'some_method'
+        self.assertEqual(hessian.name(), "analytic")
+
+
 class TestHessianClass(TestCase):
     """
     Tests for Hessian classes
diff --git a/fitbenchmarking/jacobian/tests/test_jacobian.py b/fitbenchmarking/jacobian/tests/test_jacobian.py
index f9bff665..a1b7c99e 100644
--- a/fitbenchmarking/jacobian/tests/test_jacobian.py
+++ b/fitbenchmarking/jacobian/tests/test_jacobian.py
@@ -51,7 +51,7 @@ def j(x, p):
     :rtype: 1D numpy array
     """
     return np.column_stack((np.exp(p[1] * x),
-                           x * p[0] * np.exp(p[1] * x)))
+                            x * p[0] * np.exp(p[1] * x)))
 
 
 def J(x, p):
@@ -98,6 +98,39 @@ def J_poisson(x, y, p):
                             -y*x+p[0]*x*np.exp(p[1]*x)))
 
 
+class TestJacobianName(TestCase):
+    """
+    Tests for the name function
+    """
+
+    def setUp(self):
+        """
+        Setting up tests
+        """
+        options = Options()
+        self.fitting_problem = FittingProblem(options)
+        self.fitting_problem.function = f
+        self.fitting_problem.jacobian = j
+        self.fitting_problem.data_x = np.array([1, 2, 3, 4, 5])
+        self.fitting_problem.data_y = np.array([1, 2, 4, 8, 16])
+
+    def test_scipy_jacobian(self):
+        """
+        Test the name is correct for the scipy jacobian.
+        """
+        jacobian = Scipy(self.fitting_problem)
+        jacobian.method = 'some_method'
+        self.assertEqual(jacobian.name(), "scipy some_method")
+
+    def test_analytic_jacobian(self):
+        """
+        Test the name is correct for the analytic jacobian.
+        """
+        jacobian = Analytic(self.fitting_problem)
+        jacobian.method = 'some_method'
+        self.assertEqual(jacobian.name(), "analytic")
+
+
 class TestJacobianClass(TestCase):
     """
     Tests for Jacobian classes
diff --git a/fitbenchmarking/results_processing/tests/test_fitting_report.py b/fitbenchmarking/results_processing/tests/test_fitting_report.py
index bb121ff6..21a02548 100644
--- a/fitbenchmarking/results_processing/tests/test_fitting_report.py
+++ b/fitbenchmarking/results_processing/tests/test_fitting_report.py
@@ -14,7 +14,6 @@ import unittest
 import fitbenchmarking
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
 from fitbenchmarking.parsing.fitting_problem import FittingProblem
-from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.results_processing import fitting_report
 from fitbenchmarking.utils.fitbm_result import FittingResult
 from fitbenchmarking.utils.options import Options
@@ -37,7 +36,7 @@ class CreateTests(unittest.TestCase):
         minimizers = ['min_a', 'min_b', 'min_c']
         self.results = [FittingResult(options=self.options,
                                       cost_func=c,
-                                      jac=Scipy(c),
+                                      jac='j1',
                                       hess=None,
                                       initial_params=[],
                                       params=[],
@@ -81,14 +80,14 @@ class CreateProbGroupTests(unittest.TestCase):
 
         minimizer = 'min_a'
         cost_func = NLLSCostFunc(problem)
-        jac = Scipy(cost_func)
-        jac.method = "2-point"
+        jac = 'j1'
         self.result = FittingResult(options=self.options,
                                     cost_func=cost_func,
                                     jac=jac,
                                     hess=None,
                                     initial_params=[],
                                     params=[],
+                                    software='s1',
                                     minimizer=minimizer,
                                     chi_sq=1.0001,
                                     runtime=2.0002)
@@ -114,7 +113,7 @@ class CreateProbGroupTests(unittest.TestCase):
                                          options=self.options)
         file_name = self.result.fitting_report_link
         expected = os.path.join(os.path.relpath(self.dir.name),
-                                'prob_a_nllscostfunc_min_a.html')
+                                'prob_a_nllscostfunc_min_a_[s1]_jj1.html')
 
         self.assertEqual(file_name, expected)
 
@@ -131,8 +130,7 @@ class GetFigurePathsTests(unittest.TestCase):
         problem.equation = 'equation!'
         problem.starting_values = [{'x': 1}]
         cost_func = NLLSCostFunc(problem)
-        jac = Scipy(cost_func)
-        jac.method = "2-point"
+        jac = 'j1'
         self.result = FittingResult(options=self.options,
                                     cost_func=cost_func,
                                     jac=jac,
diff --git a/fitbenchmarking/results_processing/tests/test_performance_profiler.py b/fitbenchmarking/results_processing/tests/test_performance_profiler.py
index 40ee2e94..1e5c1373 100644
--- a/fitbenchmarking/results_processing/tests/test_performance_profiler.py
+++ b/fitbenchmarking/results_processing/tests/test_performance_profiler.py
@@ -11,14 +11,13 @@ import numpy as np
 
 from fitbenchmarking.core.results_output import preprocess_data
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
-from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.parsing.fitting_problem import FittingProblem
 from fitbenchmarking.results_processing import performance_profiler
 from fitbenchmarking.utils.fitbm_result import FittingResult
 from fitbenchmarking.utils.options import Options
 
 
-class PerformanceProfillerTests(unittest.TestCase):
+class PerformanceProfilerTests(unittest.TestCase):
     """
     General tests for the performance profiler code.
     """
@@ -81,8 +80,7 @@ class PerformanceProfillerTests(unittest.TestCase):
                 list(runtime_results) / np.min(runtime_results))
             prob_results = []
             cost_func = NLLSCostFunc(problem)
-            jac = Scipy(cost_func)
-            jac.method = "2-point"
+            jac = 'j1'
             hess = None
             for j in range(self.num_minimizers):
                 minimizer = 'min_{}'.format(j)
@@ -94,6 +92,7 @@ class PerformanceProfillerTests(unittest.TestCase):
                                                   params=[1, 2, 3],
                                                   chi_sq=acc_results[j],
                                                   runtime=runtime_results[j],
+                                                  software='s1',
                                                   minimizer=minimizer))
             results.extend(prob_results)
         return results, acc_expected, runtime_expected
@@ -102,14 +101,15 @@ class PerformanceProfillerTests(unittest.TestCase):
         """
         Test that prepare profile data gives the correct result
         """
+        list(list(self.results.values())[0].values())[0][0].jacobian_tag = ''
         acc, runtime = performance_profiler.prepare_profile_data(self.results)
         acc_expected = np.array(self.acc_expected).T
         runtime_expected = np.array(self.runtime_expected).T
         acc_dict = OrderedDict()
         runtime_dict = OrderedDict()
         for j in range(self.num_minimizers):
-            acc_dict['min_{}'.format(j)] = acc_expected[j]
-            runtime_dict['min_{}'.format(j)] = runtime_expected[j]
+            acc_dict['min_{} [s1]: j:j1'.format(j)] = acc_expected[j]
+            runtime_dict['min_{} [s1]: j:j1'.format(j)] = runtime_expected[j]
         for k, v in acc_dict.items():
             assert np.allclose(v, acc[k])
         for k, v in runtime_dict.items():
diff --git a/fitbenchmarking/results_processing/tests/test_plots.py b/fitbenchmarking/results_processing/tests/test_plots.py
index f839fb97..f8a5c501 100644
--- a/fitbenchmarking/results_processing/tests/test_plots.py
+++ b/fitbenchmarking/results_processing/tests/test_plots.py
@@ -12,7 +12,6 @@ except ImportError:
     from backports.tempfile import TemporaryDirectory
 
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
-from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.parsing.fitting_problem import FittingProblem
 from fitbenchmarking.results_processing import plots
 from fitbenchmarking.utils.exceptions import PlottingError
@@ -34,12 +33,18 @@ class PlotTests(unittest.TestCase):
         self.prob.sorted_index = np.array([0, 1, 3, 2, 4])
         self.prob.data_y = np.array([4, 3, 5, 2, 1])
         self.prob.data_e = np.array([0.5, 0.2, 0.3, 0.1, 0.4])
-        self.prob.starting_values = [{'x': 1, 'y': 2}]
+        self.prob.starting_values = [{'p': 1.8}]
         self.prob.name = 'full name'
-        self.prob.eval_model = lambda y, x: y[0] * x + y[1]
+
+        def tmp(p, x=None, y=None):
+            if x is None:
+                x = self.prob.data_x
+            if y is None:
+                y = self.prob.data_y
+            return p + y * x + y
+        self.prob.eval_model = tmp
         cost_func = NLLSCostFunc(self.prob)
-        jac = Scipy(cost_func)
-        jac.method = "2-point"
+        jac = 'j1'
         self.fr = FittingResult(options=self.opts,
                                 cost_func=cost_func,
                                 jac=jac,
@@ -48,6 +53,7 @@ class PlotTests(unittest.TestCase):
                                 initial_params=[1.8],
                                 params=[1.2],
                                 runtime=2.0,
+                                software='s1',
                                 minimizer='fit',
                                 error_flag=1)
 
@@ -105,9 +111,10 @@ class PlotTests(unittest.TestCase):
         """
         Test that best plot creates a file.
         """
-        file_name = self.plot.plot_best('fit', [0.1, 3])
+        file_name = self.plot.plot_best(self.fr)
 
-        self.assertEqual(file_name, 'fit_fit_for_NLLSCostFunc_full_name.png')
+        self.assertEqual(file_name,
+                         'fit_[s1]_jj1_fit_for_NLLSCostFunc_full_name.png')
         path = os.path.join(self.dir.name, file_name)
         self.assertTrue(os.path.exists(path))
 
@@ -115,9 +122,10 @@ class PlotTests(unittest.TestCase):
         """
         Test that fit plot creates a file.
         """
-        file_name = self.plot.plot_fit('fit', [8, 6.2])
+        file_name = self.plot.plot_fit(self.fr)
 
-        self.assertEqual(file_name, 'fit_fit_for_NLLSCostFunc_full_name.png')
+        self.assertEqual(file_name,
+                         'fit_[s1]_jj1_fit_for_NLLSCostFunc_full_name.png')
         path = os.path.join(self.dir.name, file_name)
         self.assertTrue(os.path.exists(path))
 
diff --git a/fitbenchmarking/results_processing/tests/test_problem_summary_page.py b/fitbenchmarking/results_processing/tests/test_problem_summary_page.py
index 54e9646c..856af2a8 100644
--- a/fitbenchmarking/results_processing/tests/test_problem_summary_page.py
+++ b/fitbenchmarking/results_processing/tests/test_problem_summary_page.py
@@ -13,8 +13,6 @@ from fitbenchmarking.core.results_output import preprocess_data
 from fitbenchmarking.cost_func.nlls_cost_func import NLLSCostFunc
 from fitbenchmarking.cost_func.weighted_nlls_cost_func import \
     WeightedNLLSCostFunc
-from fitbenchmarking.jacobian.numdifftools_jacobian import Numdifftools
-from fitbenchmarking.jacobian.scipy_jacobian import Scipy
 from fitbenchmarking.parsing.fitting_problem import FittingProblem
 from fitbenchmarking.results_processing import problem_summary_page
 from fitbenchmarking.utils.fitbm_result import FittingResult
@@ -85,8 +83,7 @@ def generate_mock_results(results_directory: str):
 
     softwares = ['s1', 's2']
     minimizers = [['s1m1', 's1m2'], ['s2m1', 's2m2']]
-    jacobians = [[Scipy(p), Numdifftools(p)]
-                 for p in problems]
+    jacobians = [['j1', 'j2'] for _ in problems]
     cost_funcs = [[NLLSCostFunc(p), WeightedNLLSCostFunc(p)]
                   for p in problems]
 
@@ -123,8 +120,6 @@ def generate_mock_results(results_directory: str):
                 for m, minim in enumerate(minimizers[k]):
                     jacs = jacobians[i] if minim != 's1m2' else [None]
                     for n, jac in enumerate(jacs):
-                        minim_name = f'{minim}, Jac: {n}'
-                        options.minimizer_alg_type[minim_name] = 'test'
                         results.append(FittingResult(
                             options=options,
                             cost_func=cf,
@@ -136,7 +131,7 @@ def generate_mock_results(results_directory: str):
                             chi_sq=acc[i][j][k][m][n],
                             runtime=runtime[i][j][k][m][n],
                             software=software,
-                            minimizer=minim_name,
+                            minimizer=minim,
                             error_flag=None if jac is not None else 4
                         ))
 
@@ -275,8 +270,7 @@ class GetFigurePathsTests(TestCase):
         problem.equation = 'equation!'
         problem.starting_values = [{'x': 1}]
         cost_func = NLLSCostFunc(problem)
-        jac = Scipy(cost_func)
-        jac.method = "2-point"
+        jac = 'j1'
         self.result = FittingResult(options=self.options,
                                     cost_func=cost_func,
                                     jac=jac,
diff --git a/fitbenchmarking/results_processing/tests/test_tables.py b/fitbenchmarking/results_processing/tests/test_tables.py
index a5848cda..3dea426b 100644
--- a/fitbenchmarking/results_processing/tests/test_tables.py
+++ b/fitbenchmarking/results_processing/tests/test_tables.py
@@ -110,7 +110,7 @@ def generate_mock_results():
             hess = None
             r = FittingResult(options=options,
                               cost_func=cost_func,
-                              jac=jac,
+                              jac=jac.name(),
                               hess=hess,
                               initial_params=starting_values,
                               params=params_in[i][j],
@@ -120,12 +120,11 @@ def generate_mock_results():
                               software=software,
                               minimizer=options.minimizers[software][j],
                               error_flag=error_in[i][j],
+                              algorithm_type='all, ls',
                               )
             r.fitting_report_link = link_in[i][j]
             r.problem_summary_page_link = 'link0'
             results.append(r)
-            options.minimizer_alg_type[options.minimizers[software]
-                                       [j]] = 'all, ls'
         results_out.extend(results)
     best_results, results_out = preprocess_data(results_out)
     return best_results, results_out, options
diff --git a/fitbenchmarking/systests/linux_expected_results/all_parsers.txt b/fitbenchmarking/systests/linux_expected_results/all_parsers.txt
index 8d720991..4f5a2dfe 100644
--- a/fitbenchmarking/systests/linux_expected_results/all_parsers.txt
+++ b/fitbenchmarking/systests/linux_expected_results/all_parsers.txt
@@ -1,7 +1,7 @@
-                                          bumps           dfo                   gsl                levmar                             mantid            minuit                ralfit         scipy                scipy_ls
-                                       lm-bumps         dfols lmsder: scipy 2-point levmar: scipy 2-point Levenberg-Marquardt: scipy 2-point            minuit hybrid: scipy 2-point   Nelder-Mead lm-scipy: scipy 2-point
-1D sphere (synthetic neutron) IV0     239.1 (1)     239.1 (1)             239.1 (1)             239.1 (1)                          239.1 (1)         239.1 (1)             239.1 (1)     239.1 (1)               239.1 (1)
-DANWOOD                            0.001098 (1)  0.001098 (1)          0.001098 (1)          0.001098 (1)                       0.001098 (1)   0.001142 (1.04)          0.001098 (1)  0.001098 (1)            0.001098 (1)
-DanWood, Start 1                   0.001098 (1)  0.001098 (1)          0.001098 (1)          0.001098 (1)                       0.001098 (1)  0.001171 (1.066)          0.001098 (1)  0.001098 (1)            0.001098 (1)
-DanWood, Start 2                   0.001098 (1)  0.001098 (1)          0.001098 (1)          0.001098 (1)                       0.001098 (1)  0.001183 (1.078)          0.001098 (1)  0.001098 (1)            0.001098 (1)
-trig_noisy-fba                       0.7222 (1)    0.7222 (1)            0.7222 (1)            0.7222 (1)                      0.7222 (1)[2]        0.7222 (1)            0.7222 (1)    0.7222 (1)              0.7222 (1)
\ No newline at end of file
+                                          bumps           dfo                     gsl                  levmar                               mantid            minuit                  ralfit         scipy                  scipy_ls
+                                       lm-bumps         dfols lmsder: j:scipy 2-point levmar: j:scipy 2-point Levenberg-Marquardt: j:scipy 2-point            minuit hybrid: j:scipy 2-point   Nelder-Mead lm-scipy: j:scipy 2-point
+1D sphere (synthetic neutron) IV0     239.1 (1)     239.1 (1)               239.1 (1)               239.1 (1)                            239.1 (1)         239.1 (1)               239.1 (1)     239.1 (1)                 239.1 (1)
+DANWOOD                            0.001098 (1)  0.001098 (1)            0.001098 (1)            0.001098 (1)                         0.001098 (1)   0.001142 (1.04)            0.001098 (1)  0.001098 (1)              0.001098 (1)
+DanWood, Start 1                   0.001098 (1)  0.001098 (1)            0.001098 (1)            0.001098 (1)                         0.001098 (1)  0.001171 (1.066)            0.001098 (1)  0.001098 (1)              0.001098 (1)
+DanWood, Start 2                   0.001098 (1)  0.001098 (1)            0.001098 (1)            0.001098 (1)                         0.001098 (1)  0.001183 (1.078)            0.001098 (1)  0.001098 (1)              0.001098 (1)
+trig_noisy-fba                       0.7222 (1)    0.7222 (1)              0.7222 (1)              0.7222 (1)                        0.7222 (1)[2]        0.7222 (1)              0.7222 (1)    0.7222 (1)                0.7222 (1)
\ No newline at end of file
diff --git a/fitbenchmarking/systests/linux_expected_results/default_parsers.txt b/fitbenchmarking/systests/linux_expected_results/default_parsers.txt
index 3da7a716..3b821c75 100644
--- a/fitbenchmarking/systests/linux_expected_results/default_parsers.txt
+++ b/fitbenchmarking/systests/linux_expected_results/default_parsers.txt
@@ -1,5 +1,5 @@
-                        bumps                     scipy                scipy_ls
-                     lm-bumps               Nelder-Mead lm-scipy: scipy 2-point
-cubic, Start 1  3.861e-22 (1)  6.267e-05 (1.623e+17)[1]           3.861e-22 (1)
-cubic, Start 2  3.861e-22 (1)     7.176e-11 (1.859e+11)           3.861e-22 (1)
-Problem Def 1     0.03188 (1)              inf (inf)[4]            inf (inf)[4]
\ No newline at end of file
+                        bumps                     scipy                  scipy_ls
+                     lm-bumps               Nelder-Mead lm-scipy: j:scipy 2-point
+cubic, Start 1  3.861e-22 (1)  6.267e-05 (1.623e+17)[1]             3.861e-22 (1)
+cubic, Start 2  3.861e-22 (1)     7.176e-11 (1.859e+11)             3.861e-22 (1)
+Problem Def 1     0.03188 (1)              inf (inf)[4]              inf (inf)[4]
\ No newline at end of file
diff --git a/fitbenchmarking/systests/linux_expected_results/matlab.txt b/fitbenchmarking/systests/linux_expected_results/matlab.txt
index 97353e07..f902c96f 100644
--- a/fitbenchmarking/systests/linux_expected_results/matlab.txt
+++ b/fitbenchmarking/systests/linux_expected_results/matlab.txt
@@ -1,6 +1,6 @@
-                                 matlab           matlab_curve                         matlab_opt        matlab_stats
-                    Nelder-Mead Simplex    Levenberg-Marquardt levenberg-marquardt: scipy 2-point Levenberg-Marquardt
-BENNETT5            1.66e-05 (1.024)[1]       1.652e-05 (1.02)                   1.652e-05 (1.02)        1.62e-05 (1)
-cubic, Start 1  0.003982 (1.031e+19)[1]  9.158e-15 (2.372e+07)              9.158e-15 (2.372e+07)       3.862e-22 (1)
-cubic, Start 2    7.176e-11 (1.859e+11)  1.593e-17 (4.126e+04)              1.592e-17 (4.124e+04)       3.861e-22 (1)
-Problem Def 1              inf (inf)[4]   22.45 (1.729e+11)[1]                   1.299e-10 (1)[2]        inf (inf)[4]
\ No newline at end of file
+                                 matlab           matlab_curve                           matlab_opt        matlab_stats
+                    Nelder-Mead Simplex    Levenberg-Marquardt levenberg-marquardt: j:scipy 2-point Levenberg-Marquardt
+BENNETT5            1.66e-05 (1.024)[1]       1.652e-05 (1.02)                     1.652e-05 (1.02)        1.62e-05 (1)
+cubic, Start 1  0.003982 (1.031e+19)[1]  9.158e-15 (2.372e+07)                9.158e-15 (2.372e+07)       3.862e-22 (1)
+cubic, Start 2    7.176e-11 (1.859e+11)  1.593e-17 (4.126e+04)                1.592e-17 (4.124e+04)       3.861e-22 (1)
+Problem Def 1              inf (inf)[4]   22.45 (1.729e+11)[1]                     1.299e-10 (1)[2]        inf (inf)[4]
\ No newline at end of file
diff --git a/fitbenchmarking/systests/linux_expected_results/multifit.txt b/fitbenchmarking/systests/linux_expected_results/multifit.txt
index 7e51383f..15321228 100644
--- a/fitbenchmarking/systests/linux_expected_results/multifit.txt
+++ b/fitbenchmarking/systests/linux_expected_results/multifit.txt
@@ -1,4 +1,4 @@
-                                                      mantid                                   
-                          Levenberg-Marquardt: scipy 2-point Levenberg-Marquardt: scipy 3-point
-Basic MultiFit, Dataset 1                           8.16 (1)                           8.16 (1)
-Basic MultiFit, Dataset 2                          15.28 (1)                          15.28 (1)
\ No newline at end of file
+                                                        mantid                                     
+                          Levenberg-Marquardt: j:scipy 2-point Levenberg-Marquardt: j:scipy 3-point
+Basic MultiFit, Dataset 1                             8.16 (1)                             8.16 (1)
+Basic MultiFit, Dataset 2                            15.28 (1)                            15.28 (1)
\ No newline at end of file
diff --git a/fitbenchmarking/utils/tests/test_fitbm_result.py b/fitbenchmarking/utils/tests/test_fitbm_result.py
index fa96bca5..8e9cf7b4 100644
--- a/fitbenchmarking/utils/tests/test_fitbm_result.py
+++ b/fitbenchmarking/utils/tests/test_fitbm_result.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function
 
 import inspect
 import os
+import textwrap
 import unittest
 
 import numpy as np
@@ -40,14 +41,14 @@ class FitbmResultTests(unittest.TestCase):
         self.params = np.array([1, 3, 4, 4])
         self.initial_params = np.array([0, 0, 0, 0])
         self.cost_func = NLLSCostFunc(self.problem)
-        self.jac = Scipy(self.cost_func)
-        self.jac.method = "2-point"
-        self.hess = AnalyticHessian(self.cost_func.problem, self.jac)
+        self.jac = 'jac1'
+        self.hess = 'hess1'
         self.result = FittingResult(
             options=self.options, cost_func=self.cost_func, jac=self.jac,
             hess=self.hess, chi_sq=self.chi_sq, runtime=self.runtime,
-            minimizer=self.minimizer, initial_params=self.initial_params,
-            params=self.params, error_flag=0)
+            minimizer=self.minimizer, software='s1',
+            initial_params=self.initial_params, params=self.params,
+            error_flag=0)
 
         self.min_chi_sq = 0.1
         self.result.min_chi_sq = self.min_chi_sq
@@ -58,26 +59,32 @@ class FitbmResultTests(unittest.TestCase):
         """
         Test that the fitting result can be printed as a readable string.
         """
-        self.assertEqual(str(self.result),
-                         "+================================+\n"
-                         "| FittingResult                  |\n"
-                         "+================================+\n"
-                         "| Cost Function | NLLSCostFunc   |\n"
-                         "+--------------------------------+\n"
-                         "| Problem       | cubic          |\n"
-                         "+--------------------------------+\n"
-                         "| Software      | None           |\n"
-                         "+--------------------------------+\n"
-                         "| Minimizer     | test_minimizer |\n"
-                         "+--------------------------------+\n"
-                         "| Jacobian      | Scipy          |\n"
-                         "+--------------------------------+\n"
-                         "| Hessian       | Analytic       |\n"
-                         "+--------------------------------+\n"
-                         "| Chi Squared   | 10             |\n"
-                         "+--------------------------------+\n"
-                         "| Runtime       | 0.01           |\n"
-                         "+--------------------------------+")
+        expected = textwrap.dedent('''\
+            +================================+
+            | FittingResult                  |
+            +================================+
+            | Cost Function | NLLSCostFunc   |
+            +--------------------------------+
+            | Problem       | cubic          |
+            +--------------------------------+
+            | Software      | s1             |
+            +--------------------------------+
+            | Minimizer     | test_minimizer |
+            +--------------------------------+
+            | Jacobian      | jac1           |
+            +--------------------------------+
+            | Hessian       | hess1          |
+            +--------------------------------+
+            | Chi Squared   | 10             |
+            +--------------------------------+
+            | Runtime       | 0.01           |
+            +--------------------------------+''')
+
+        for i, (r, e) in enumerate(zip(str(self.result).splitlines(),
+                                       expected.splitlines())):
+            if r != e:
+                print(f'Issue on line {i}:\n> {r}\n<{e}')
+        self.assertEqual(str(self.result), expected)
 
     def test_init_with_dataset_id(self):
         """
@@ -165,13 +172,49 @@ class FitbmResultTests(unittest.TestCase):
         self.result.name = 'test, name with commas'
         self.assertEqual(self.result.sanitised_name, 'test_name_with_commas')
 
-    def test_sanitised_min_name(self):
+    def test_modified_minimizer_name_no_software(self):
+        """
+        Test modified minimizer name is correct when not including software.
+        """
+        self.result.software_tag = 's1'
+        self.result.minimizer_tag = 'm1'
+        self.result.jacobian_tag = 'j1'
+        self.result.hessian_tag = 'h1'
+        self.assertEqual(self.result.modified_minimizer_name(False),
+                         'm1: j:j1 h:h1')
+
+    def test_modified_minimizer_name_with_software(self):
+        """
+        Test modified minimizer name is correct when including software.
+        """
+        self.result.software_tag = 's1'
+        self.result.minimizer_tag = 'm1'
+        self.result.jacobian_tag = 'j1'
+        self.result.hessian_tag = 'h1'
+        self.assertEqual(self.result.modified_minimizer_name(True),
+                         'm1 [s1]: j:j1 h:h1')
+
+    def test_sanitised_min_name_no_software(self):
+        """
+        Test that sanitised minimizer names are correct without software.
+        """
+        self.result.software_tag = 's1'
+        self.result.minimizer_tag = 'm1'
+        self.result.jacobian_tag = 'j1'
+        self.result.hessian_tag = 'h1'
+        self.assertEqual(self.result.sanitised_min_name(False),
+                         'm1_jj1_hh1')
+
+    def test_sanitised_min_name_with_software(self):
         """
-        Test that sanitised minimizer names are correct.
+        Test that sanitised minimizer names are correct with software.
         """
-        self.result.minimizer = 'test: name with colon'
-        self.assertEqual(self.result.sanitised_min_name,
-                         'test_name_with_colon')
+        self.result.software_tag = 's1'
+        self.result.minimizer_tag = 'm1'
+        self.result.jacobian_tag = 'j1'
+        self.result.hessian_tag = 'h1'
+        self.assertEqual(self.result.sanitised_min_name(True),
+                         'm1_[s1]_jj1_hh1')
 
 
 if __name__ == "__main__":