Index: /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression-3.4.csproj
===================================================================
--- /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression-3.4.csproj	(revision 18052)
+++ /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression-3.4.csproj	(revision 18053)
@@ -133,5 +133,6 @@
     <Compile Include="SingleObjective\Evaluators\SymbolicRegressionMeanRelativeErrorEvaluator.cs" />
     <Compile Include="SingleObjective\ShapeConstrainedRegressionSingleObjectiveProblem.cs" />
-    <Compile Include="SingleObjective\SymbolicRegressionSolutionsAnalyzer.cs" />
+    <Compile Include="SingleObjective\SolutionQualityAnalyzer.cs" />
+    <Compile Include="SingleObjective\SolutionRSquaredAnalyzer.cs" />
     <Compile Include="SymbolicRegressionPhenotypicDiversityAnalyzer.cs" />
     <Compile Include="SymbolicRegressionPruningAnalyzer.cs" />
Index: /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionQualityAnalyzer.cs
===================================================================
--- /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionQualityAnalyzer.cs	(revision 18053)
+++ /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionQualityAnalyzer.cs	(revision 18053)
@@ -0,0 +1,129 @@
+﻿#region License Information
+
+/* HeuristicLab
+ * Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL)
+ *
+ * This file is part of HeuristicLab.
+ *
+ * HeuristicLab is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * HeuristicLab is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#endregion
+
+using System.Linq;
+using HeuristicLab.Analysis;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Data;
+using HeuristicLab.Operators;
+using HeuristicLab.Optimization;
+using HeuristicLab.Parameters;
+using HEAL.Attic;
+using System;
+
+namespace HeuristicLab.Problems.DataAnalysis.Symbolic.Regression {
+  /// <summary>
+  /// Finds all RegressionSolutions in a ResultCollection and extracts the DoubleValue of a result (an optionally a secondary result) for each solution.
+  /// All values are tracked in a single DataTable over generations.
+  /// This can be used to extract training and test NMSE for the best solution for a chart of training/test NMSE over generations.
+  /// </summary>
+  [StorableType("52047D25-C042-4773-B12D-AB67DD707AE6")]
+  public sealed class SolutionQualityAnalyzer : SingleSuccessorOperator, IAnalyzer {
+    private const string ResultCollectionParameterName = "Results";
+    private const string SolutionResultsResultName = "Solution Results";
+    private const string DefaultTrainingResultName = "Normalized mean squared error (training)";
+    private const string DefaultTestResultName = "Normalized mean squared error (test)"; // use NMSE instead of R². R² is too optimistic for the test (assumes perfect scaling).
+
+    public ILookupParameter<ResultCollection> ResultCollectionParameter {
+      get { return (ILookupParameter<ResultCollection>)Parameters[ResultCollectionParameterName]; }
+    }
+    public ILookupParameter<DoubleValue> ResultParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters[DefaultTrainingResultName]; }
+    }
+    public ILookupParameter<DoubleValue> SecondaryResultParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters[DefaultTestResultName]; }
+    }
+
+    public bool EnabledByDefault {
+      get { return false; }
+    }
+
+    [StorableConstructor]
+    private SolutionQualityAnalyzer(StorableConstructorFlag _) : base(_) { }
+    private SolutionQualityAnalyzer(SolutionQualityAnalyzer original, Cloner cloner)
+      : base(original, cloner) { }
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new SolutionQualityAnalyzer(this, cloner);
+    }
+
+    public SolutionQualityAnalyzer() {
+      Parameters.Add(new LookupParameter<ResultCollection>(ResultCollectionParameterName, "The result collection to store the analysis results."));
+      Parameters.Add(new LookupParameter<DoubleValue>(DefaultTrainingResultName, $"First solution result name (default: {DefaultTrainingResultName})."));
+      Parameters.Add(new LookupParameter<DoubleValue>(DefaultTestResultName, $"Second solution result name (default: {DefaultTestResultName}). This is optional. Leave empty if not used."));
+    }
+
+    public override IOperation Apply() {
+      var results = ResultCollectionParameter.ActualValue;
+
+      if (!results.ContainsKey(SolutionResultsResultName)) {
+        var newDataTable = new DataTable(SolutionResultsResultName);
+        results.Add(new Result(SolutionResultsResultName, "Chart of solution result values of all regression solutions.", newDataTable));
+      }
+
+      var dataTable = (DataTable)results[SolutionResultsResultName].Value;
+
+      // store actual names of parameter because they are changed below
+      var resultParameter = ResultParameter;
+      string savedResultParamName = resultParameter.ActualName;
+      var secondaryResultParameter = SecondaryResultParameter;
+      string savedSecondaryResultParamName = secondaryResultParameter.ActualName;
+      try {
+        foreach (var result in results.Where(r => r.Value is RegressionSolution)) {
+          var solution = (RegressionSolution)result.Value;
+
+          var resultRowName = result.Name + " " + savedResultParamName;
+          var resultValue = solution[savedResultParamName].Value as DoubleValue;
+          if (resultValue == null) throw new ArgumentException($"SolutionQualityAnalyzer: the selected result ({savedResultParamName}) is no DoubleValue");
+
+          if (!dataTable.Rows.ContainsKey(resultRowName))
+            dataTable.Rows.Add(new DataRow(resultRowName));
+
+          dataTable.Rows[resultRowName].Values.Add(resultValue.Value);
+          // HACK: we change the ActualName of the parameter to write the results for each solution to the scope
+          resultParameter.ActualName = resultRowName;
+          resultParameter.ActualValue = new DoubleValue(resultValue.Value);
+
+          // same for the second result if it is given
+          if (!string.IsNullOrWhiteSpace(savedSecondaryResultParamName)) {
+            var secondaryResultRowName = result.Name + " " + savedSecondaryResultParamName;
+            var secondaryResultValue = solution[savedSecondaryResultParamName].Value as DoubleValue;
+            if (secondaryResultValue == null) throw new ArgumentException($"SolutionQualityAnalyzer: the selected result ({savedSecondaryResultParamName}) is no DoubleValue");
+
+            if (!dataTable.Rows.ContainsKey(secondaryResultRowName))
+              dataTable.Rows.Add(new DataRow(secondaryResultRowName));
+
+            dataTable.Rows[secondaryResultRowName].Values.Add(secondaryResultValue.Value);
+            secondaryResultParameter.ActualName = secondaryResultRowName;
+            secondaryResultParameter.ActualValue = new DoubleValue(secondaryResultValue.Value);
+          }
+        }
+      } finally {
+        // restore saved parameter names
+        resultParameter.ActualName = savedResultParamName;
+        secondaryResultParameter.ActualName = savedSecondaryResultParamName;
+      }
+      return base.Apply();
+    }
+  }
+}
Index: /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionRSquaredAnalyzer.cs
===================================================================
--- /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionRSquaredAnalyzer.cs	(revision 18053)
+++ /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SolutionRSquaredAnalyzer.cs	(revision 18053)
@@ -0,0 +1,130 @@
+﻿#region License Information
+
+/* HeuristicLab
+ * Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL)
+ *
+ * This file is part of HeuristicLab.
+ *
+ * HeuristicLab is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * HeuristicLab is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#endregion
+
+using System.Linq;
+using HeuristicLab.Analysis;
+using HeuristicLab.Common;
+using HeuristicLab.Core;
+using HeuristicLab.Data;
+using HeuristicLab.Operators;
+using HeuristicLab.Optimization;
+using HeuristicLab.Parameters;
+using HEAL.Attic;
+using System;
+
+namespace HeuristicLab.Problems.DataAnalysis.Symbolic.Regression {
+  [StorableType("789E0217-6DDC-44E8-85CC-A51A976A8FB8")]
+  [Obsolete("Use SolutionQualityAnalyzer instead")]
+  public class SolutionRSquaredAnalyzer : SingleSuccessorOperator, IAnalyzer {
+    private const string ResultCollectionParameterName = "Results";
+    private const string RegressionSolutionQualitiesResultName = "Regression Solution Qualities";
+    private const string TrainingQualityParameterName = "TrainingRSquared";
+    private const string TestQualityParameterName = "TestRSquared";
+
+    public ILookupParameter<ResultCollection> ResultCollectionParameter {
+      get { return (ILookupParameter<ResultCollection>)Parameters[ResultCollectionParameterName]; }
+    }
+    public ILookupParameter<DoubleValue> TrainingQualityParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters[TrainingQualityParameterName]; }
+    }
+    public ILookupParameter<DoubleValue> TestQualityParameter {
+      get { return (ILookupParameter<DoubleValue>)Parameters[TestQualityParameterName]; }
+    }
+
+    public virtual bool EnabledByDefault {
+      get { return false; }
+    }
+
+    [StorableConstructor]
+    protected SolutionRSquaredAnalyzer(StorableConstructorFlag _) : base(_) { }
+    protected SolutionRSquaredAnalyzer(SolutionRSquaredAnalyzer original, Cloner cloner)
+      : base(original, cloner) { }
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new SolutionRSquaredAnalyzer(this, cloner);
+    }
+
+    public SolutionRSquaredAnalyzer() {
+      Parameters.Add(new LookupParameter<ResultCollection>(ResultCollectionParameterName, "The result collection to store the analysis results."));
+      Parameters.Add(new LookupParameter<DoubleValue>(TrainingQualityParameterName));
+      Parameters.Add(new LookupParameter<DoubleValue>(TestQualityParameterName));
+    }
+
+    [StorableHook(HookType.AfterDeserialization)]
+    private void AfterDeserialization() {
+      // BackwardsCompatibility3.3
+
+      #region Backwards compatible code, remove with 3.4
+      if (!Parameters.ContainsKey(TrainingQualityParameterName))
+        Parameters.Add(new LookupParameter<DoubleValue>(TrainingQualityParameterName));
+      if (!Parameters.ContainsKey(TestQualityParameterName))
+        Parameters.Add(new LookupParameter<DoubleValue>(TestQualityParameterName));
+      #endregion
+    }
+
+    public override IOperation Apply() {
+      var results = ResultCollectionParameter.ActualValue;
+
+      if (!results.ContainsKey(RegressionSolutionQualitiesResultName)) {
+        var newDataTable = new DataTable(RegressionSolutionQualitiesResultName);
+        results.Add(new Result(RegressionSolutionQualitiesResultName, "Chart displaying the training and test qualities of the regression solutions.", newDataTable));
+      }
+
+      var dataTable = (DataTable)results[RegressionSolutionQualitiesResultName].Value;
+
+      // only if the parameters are available (not available in old persisted code)
+      ILookupParameter<DoubleValue> trainingQualityParam = null;
+      ILookupParameter<DoubleValue> testQualityParam = null;
+      // store actual names of parameter because they are changed below
+      trainingQualityParam = TrainingQualityParameter;
+      string prevTrainingQualityParamName = trainingQualityParam.ActualName;
+      testQualityParam = TestQualityParameter;
+      string prevTestQualityParamName = testQualityParam.ActualName;
+      foreach (var result in results.Where(r => r.Value is IRegressionSolution)) {
+        var solution = (IRegressionSolution)result.Value;
+
+        var trainingR2Name = result.Name + " Training R²";
+        if (!dataTable.Rows.ContainsKey(trainingR2Name))
+          dataTable.Rows.Add(new DataRow(trainingR2Name));
+
+        var testR2Name = result.Name + " Test R²";
+        if (!dataTable.Rows.ContainsKey(testR2Name))
+          dataTable.Rows.Add(new DataRow(testR2Name));
+
+        dataTable.Rows[trainingR2Name].Values.Add(solution.TrainingRSquared);
+        dataTable.Rows[testR2Name].Values.Add(solution.TestRSquared);
+
+        // also add training and test R² to the scope using the parameters
+        // HACK: we change the ActualName of the parameter to write training & test quality for each solution in the results collection
+        trainingQualityParam.ActualName = trainingR2Name;
+        trainingQualityParam.ActualValue = new DoubleValue(solution.TrainingRSquared);
+        testQualityParam.ActualName = testR2Name;
+        testQualityParam.ActualValue = new DoubleValue(solution.TestRSquared);
+      }
+
+      trainingQualityParam.ActualName = prevTrainingQualityParamName;
+      testQualityParam.ActualName = prevTestQualityParamName;
+
+      return base.Apply();
+    }
+  }
+}
Index: /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SymbolicRegressionSingleObjectiveProblem.cs
===================================================================
--- /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SymbolicRegressionSingleObjectiveProblem.cs	(revision 18052)
+++ /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SymbolicRegressionSingleObjectiveProblem.cs	(revision 18053)
@@ -90,6 +90,6 @@
         changed = true;
       }
-      if (!Operators.OfType<SymbolicRegressionSolutionsAnalyzer>().Any()) {
-        Operators.Add(new SymbolicRegressionSolutionsAnalyzer());
+      if (!Operators.OfType<SolutionQualityAnalyzer>().Any()) {
+        Operators.Add(new SolutionQualityAnalyzer());
         changed = true;
       }
@@ -119,5 +119,5 @@
       Operators.Add(new SymbolicRegressionSingleObjectiveTrainingParetoBestSolutionAnalyzer());
       Operators.Add(new SymbolicRegressionSingleObjectiveValidationParetoBestSolutionAnalyzer());
-      Operators.Add(new SymbolicRegressionSolutionsAnalyzer());
+      Operators.Add(new SolutionQualityAnalyzer());
       Operators.Add(new SymbolicExpressionTreePhenotypicSimilarityCalculator());
       Operators.Add(new ShapeConstraintsAnalyzer());
Index: unk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SymbolicRegressionSolutionsAnalyzer.cs
===================================================================
--- /trunk/HeuristicLab.Problems.DataAnalysis.Symbolic.Regression/3.4/SingleObjective/SymbolicRegressionSolutionsAnalyzer.cs	(revision 18052)
+++ 	(revision )
@@ -1,128 +1,0 @@
-﻿#region License Information
-
-/* HeuristicLab
- * Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL)
- *
- * This file is part of HeuristicLab.
- *
- * HeuristicLab is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * HeuristicLab is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#endregion
-
-using System.Linq;
-using HeuristicLab.Analysis;
-using HeuristicLab.Common;
-using HeuristicLab.Core;
-using HeuristicLab.Data;
-using HeuristicLab.Operators;
-using HeuristicLab.Optimization;
-using HeuristicLab.Parameters;
-using HEAL.Attic;
-
-namespace HeuristicLab.Problems.DataAnalysis.Symbolic.Regression {
-  [StorableType("789E0217-6DDC-44E8-85CC-A51A976A8FB8")]
-  public class SymbolicRegressionSolutionsAnalyzer : SingleSuccessorOperator, IAnalyzer {
-    private const string ResultCollectionParameterName = "Results";
-    private const string RegressionSolutionQualitiesResultName = "Regression Solution Qualities";
-    private const string TrainingQualityParameterName = "TrainingRSquared";
-    private const string TestQualityParameterName = "TestRSquared";
-
-    public ILookupParameter<ResultCollection> ResultCollectionParameter {
-      get { return (ILookupParameter<ResultCollection>)Parameters[ResultCollectionParameterName]; }
-    }
-    public ILookupParameter<DoubleValue> TrainingQualityParameter {
-      get { return (ILookupParameter<DoubleValue>)Parameters[TrainingQualityParameterName]; }
-    }
-    public ILookupParameter<DoubleValue> TestQualityParameter {
-      get { return (ILookupParameter<DoubleValue>)Parameters[TestQualityParameterName]; }
-    }
-
-    public virtual bool EnabledByDefault {
-      get { return false; }
-    }
-
-    [StorableConstructor]
-    protected SymbolicRegressionSolutionsAnalyzer(StorableConstructorFlag _) : base(_) { }
-    protected SymbolicRegressionSolutionsAnalyzer(SymbolicRegressionSolutionsAnalyzer original, Cloner cloner)
-      : base(original, cloner) { }
-    public override IDeepCloneable Clone(Cloner cloner) {
-      return new SymbolicRegressionSolutionsAnalyzer(this, cloner);
-    }
-
-    public SymbolicRegressionSolutionsAnalyzer() {
-      Parameters.Add(new LookupParameter<ResultCollection>(ResultCollectionParameterName, "The result collection to store the analysis results."));
-      Parameters.Add(new LookupParameter<DoubleValue>(TrainingQualityParameterName));
-      Parameters.Add(new LookupParameter<DoubleValue>(TestQualityParameterName));
-    }
-
-    [StorableHook(HookType.AfterDeserialization)]
-    private void AfterDeserialization() {
-      // BackwardsCompatibility3.3
-
-      #region Backwards compatible code, remove with 3.4
-      if (!Parameters.ContainsKey(TrainingQualityParameterName))
-        Parameters.Add(new LookupParameter<DoubleValue>(TrainingQualityParameterName));
-      if (!Parameters.ContainsKey(TestQualityParameterName))
-        Parameters.Add(new LookupParameter<DoubleValue>(TestQualityParameterName));
-      #endregion
-    }
-
-    public override IOperation Apply() {
-      var results = ResultCollectionParameter.ActualValue;
-
-      if (!results.ContainsKey(RegressionSolutionQualitiesResultName)) {
-        var newDataTable = new DataTable(RegressionSolutionQualitiesResultName);
-        results.Add(new Result(RegressionSolutionQualitiesResultName, "Chart displaying the training and test qualities of the regression solutions.", newDataTable));
-      }
-
-      var dataTable = (DataTable)results[RegressionSolutionQualitiesResultName].Value;
-
-      // only if the parameters are available (not available in old persisted code)
-      ILookupParameter<DoubleValue> trainingQualityParam = null;
-      ILookupParameter<DoubleValue> testQualityParam = null;
-      // store actual names of parameter because it is changed below
-      trainingQualityParam = TrainingQualityParameter;
-      string prevTrainingQualityParamName = trainingQualityParam.ActualName;
-      testQualityParam = TestQualityParameter;
-      string prevTestQualityParamName = testQualityParam.ActualName;
-      foreach (var result in results.Where(r => r.Value is IRegressionSolution)) {
-        var solution = (IRegressionSolution)result.Value;
-
-        var trainingR2Name = result.Name + " Training R²";
-        if (!dataTable.Rows.ContainsKey(trainingR2Name))
-          dataTable.Rows.Add(new DataRow(trainingR2Name));
-
-        var testR2Name = result.Name + " Test R²";
-        if (!dataTable.Rows.ContainsKey(testR2Name))
-          dataTable.Rows.Add(new DataRow(testR2Name));
-
-        dataTable.Rows[trainingR2Name].Values.Add(solution.TrainingRSquared);
-        dataTable.Rows[testR2Name].Values.Add(solution.TestRSquared);
-
-        // also add training and test R² to the scope using the parameters
-        // HACK: we change the ActualName of the parameter to write two variables for each solution in the results collection
-        trainingQualityParam.ActualName = trainingR2Name;
-        trainingQualityParam.ActualValue = new DoubleValue(solution.TrainingRSquared);
-        testQualityParam.ActualName = testR2Name;
-        testQualityParam.ActualValue = new DoubleValue(solution.TestRSquared);
-      }
-
-      trainingQualityParam.ActualName = prevTrainingQualityParamName;
-      testQualityParam.ActualName = prevTestQualityParamName;
-
-      return base.Apply();
-    }
-  }
-}
