From a516d01e65b7f215357dad380fb46ad978e745fe Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 20:08:56 +0100 Subject: [PATCH 1/6] fix: Update ROOT to v6.36.08-pre to fix bugs with analytical Hessian Relevant ROOT patches: 1. 84cca5ef770e677f88955a83759158a4db3fd0dd [Minuit2] Fix external index usage in analytical Hessian Fix external index usage in analytical Hessian and also add a unit test to cover this former bug. This problem exists since the introduction of external Hessians to Minuit2 in 888a7677f7b4e7a in the ROOT 6.28 development cycle. This bug was discovered while investigating #20913. 2. 4ffe39c8d3baeb1476316ac50ccfe822758388b3 [Minuit2] Fallback to full Hessian in AnalyticalGradientCalc. if no G2 If the user function doesn't claim to implement the diagonal of the Hessian in an efficient way by overriding `HasG2()` to return `true`, the AnalyticalGradientCalculator should not call `G2()`, but instead fall back to extract the diagonal from the full Hessian. Closes #20913. --- Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.targets b/Build.targets index 3133625..d64b1f6 100644 --- a/Build.targets +++ b/Build.targets @@ -1,7 +1,7 @@  - b5fd6643c1cc36fa95297c6a5cda4140573006d5 + 3b5475ca350bd5e50b58fd2d8642051ee0c19916 $(MSBuildThisFileDirectory)minuit2.net/obj/root $(MSBuildThisFileDirectory)/minuit2.net/obj/gen From f36511f0d7a7774b29f7b2e3ad197dc5e21b91c6 Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 20:10:27 +0100 Subject: [PATCH 2/6] test: Extend benchmarks for binding kinetics problem to all derivative configurations --- .../SurfaceBiosensorBindingKineticsMigradBenchmarks.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/minuit2.net.Benchmarks/SurfaceBiosensorBindingKineticsMigradBenchmarks.cs b/test/minuit2.net.Benchmarks/SurfaceBiosensorBindingKineticsMigradBenchmarks.cs index 7952ae9..319c7a5 100644 --- a/test/minuit2.net.Benchmarks/SurfaceBiosensorBindingKineticsMigradBenchmarks.cs +++ b/test/minuit2.net.Benchmarks/SurfaceBiosensorBindingKineticsMigradBenchmarks.cs @@ -12,9 +12,7 @@ namespace minuit2.net.Benchmarks; [Orderer(SummaryOrderPolicy.Method)] public class SurfaceBiosensorBindingKineticsMigradBenchmarks { - [Params(WithoutDerivatives, WithGradient)] - // Benchmark all configurations once Hessian-indexing bug is fixed in Minuit2 - // (see https://github.com/root-project/root/pull/20936) + [Params(WithoutDerivatives, WithGradient, WithGradientAndHessian, WithGradientHessianAndHessianDiagonal)] public DerivativeConfiguration DerivativeConfiguration; [Params(Fast, Balanced, Rigorous, VeryRigorous)] From e528f9c0df5d3e0d5811c69830b4911f75f4dd24 Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 20:29:17 +0100 Subject: [PATCH 3/6] refactor: Remove code esnuring least squares cost Hessian diagonal is evaluated when only full model Hessian is provided This was a workaround for the "G2 bug" in Minuit2 that is fixed now (see commit a516d01e65b7f215357dad380fb46ad978e745fe). When no model Hessian diagonal is provided, the Minuit2 code now has the expected behavior: The cost function's G2 (= Hessian diagonal), when needed, is extracted from the full Hessian, so there is no need to duplicate that logic in the C# code. --- minuit2.net/CostFunctions/LeastSquares.cs | 26 --------- minuit2.net/CostFunctions/LeastSquaresBase.cs | 2 +- .../Least_squares_cost_function.spec.cs | 54 ------------------- 3 files changed, 1 insertion(+), 81 deletions(-) diff --git a/minuit2.net/CostFunctions/LeastSquares.cs b/minuit2.net/CostFunctions/LeastSquares.cs index b7e312b..b93faac 100644 --- a/minuit2.net/CostFunctions/LeastSquares.cs +++ b/minuit2.net/CostFunctions/LeastSquares.cs @@ -68,32 +68,6 @@ public override IReadOnlyList HessianFor(IReadOnlyList parameter } public override IReadOnlyList HessianDiagonalFor(IReadOnlyList parameterValues) - { - return modelHessianDiagonal == null - ? HessianDiagonalFromModelHessianFor(parameterValues) - : HessianDiagonalFromModelHessianDiagonalFor(parameterValues); - } - - private double[] HessianDiagonalFromModelHessianFor(IReadOnlyList parameterValues) - { - var hessianDiagonal = new double[Parameters.Count]; - for (var i = 0; i < x.Count; i++) - { - var yError = yErrorForIndex(i); - var r = (y[i] - model(x[i], parameterValues)) / yError; - var g = modelGradient!(x[i], parameterValues); - var h = modelHessian!(x[i], parameterValues); - for (var j = 0; j < Parameters.Count; j++) - { - var jj = j * (Parameters.Count + 1); - hessianDiagonal[j] -= 2 / yError * (r * h[jj] - g[j] * g[j] / yError); - } - } - - return hessianDiagonal; - } - - private double[] HessianDiagonalFromModelHessianDiagonalFor(IReadOnlyList parameterValues) { var hessianDiagonal = new double[Parameters.Count]; for (var i = 0; i < x.Count; i++) diff --git a/minuit2.net/CostFunctions/LeastSquaresBase.cs b/minuit2.net/CostFunctions/LeastSquaresBase.cs index c962321..4cf395f 100644 --- a/minuit2.net/CostFunctions/LeastSquaresBase.cs +++ b/minuit2.net/CostFunctions/LeastSquaresBase.cs @@ -12,7 +12,7 @@ internal abstract class LeastSquaresBase( public IReadOnlyList Parameters { get; } = parameters; public bool HasGradient { get; } = hasModelGradient; public bool HasHessian { get; } = hasModelGradient && hasModelHessian; - public bool HasHessianDiagonal { get; } = hasModelGradient && (hasModelHessianDiagonal || hasModelHessian); + public bool HasHessianDiagonal { get; } = hasModelGradient && hasModelHessianDiagonal; public double ErrorDefinition { get; } = errorDefinition; // For least squares fits, an error definition of 1 corresponds to 1-sigma parameter errors diff --git a/test/minuit2.net.UnitTests/Least_squares_cost_function.spec.cs b/test/minuit2.net.UnitTests/Least_squares_cost_function.spec.cs index 7ba6b0c..ebfec00 100644 --- a/test/minuit2.net.UnitTests/Least_squares_cost_function.spec.cs +++ b/test/minuit2.net.UnitTests/Least_squares_cost_function.spec.cs @@ -129,24 +129,6 @@ public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian options => options.WithSmartDoubleTolerance(0.001)); } - [Test] - public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() - { - var cost = LeastSquares(_xValues, _yValues, _yError, _parameters, TestModel, TestModelGradient, TestModelHessian); - - var expectedHessianDiagonal = new double[2]; - for (var i = 0; i < _valueCount; i++) - { - var g = TestModelGradient(_xValues[i], _parameterValues); - var h = TestModelHessian(_xValues[i], _parameterValues); - expectedHessianDiagonal[0] -= 2 / _yError * (Residual(i) * h[0] - g[0] * g[0] / _yError); - expectedHessianDiagonal[1] -= 2 / _yError * (Residual(i) * h[3] - g[1] * g[1] / _yError); - } - - cost.HessianDiagonalFor(_parameterValues).Should().BeEquivalentTo(expectedHessianDiagonal, - options => options.WithSmartDoubleTolerance(0.001)); - } - [Test] public void and_analytical_model_gradient_and_hessian_and_hessian_diagonal_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() { @@ -309,24 +291,6 @@ public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian options => options.WithSmartDoubleTolerance(0.001)); } - [Test] - public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() - { - var cost = LeastSquares(_xValues, _yValues, _yErrors, _parameters, TestModel, TestModelGradient, TestModelHessian); - - var expectedHessianDiagonal = new double[2]; - for (var i = 0; i < _valueCount; i++) - { - var g = TestModelGradient(_xValues[i], _parameterValues); - var h = TestModelHessian(_xValues[i], _parameterValues); - expectedHessianDiagonal[0] -= 2 / _yErrors[i] * (Residual(i) * h[0] - g[0] * g[0] / _yErrors[i]); - expectedHessianDiagonal[1] -= 2 / _yErrors[i] * (Residual(i) * h[3] - g[1] * g[1] / _yErrors[i]); - } - - cost.HessianDiagonalFor(_parameterValues).Should().BeEquivalentTo(expectedHessianDiagonal, - options => options.WithSmartDoubleTolerance(0.001)); - } - [Test] public void and_analytical_model_gradient_and_hessian_and_hessian_diagonal_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() { @@ -477,24 +441,6 @@ public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian options => options.WithSmartDoubleTolerance(0.001)); } - [Test] - public void and_analytical_model_gradient_and_hessian_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() - { - var cost = LeastSquares(_xValues, _yValues, _parameters, TestModel, TestModelGradient, TestModelHessian); - - var expectedHessianDiagonal = new double[2]; - for (var i = 0; i < _valueCount; i++) - { - var g = TestModelGradient(_xValues[i], _parameterValues); - var h = TestModelHessian(_xValues[i], _parameterValues); - expectedHessianDiagonal[0] -= 2 * (Residual(i) * h[0] - g[0] * g[0]); - expectedHessianDiagonal[1] -= 2 * (Residual(i) * h[3] - g[1] * g[1]); - } - - cost.HessianDiagonalFor(_parameterValues).Should().BeEquivalentTo(expectedHessianDiagonal, - options => options.WithSmartDoubleTolerance(0.001)); - } - [Test] public void and_analytical_model_gradient_and_hessian_and_hessian_diagonal_when_asked_for_its_hessian_diagonal_returns_the_expected_vector() { From e86c6bbd38b263b84d98f52b1ab16ba6a3edae5f Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 20:39:35 +0100 Subject: [PATCH 4/6] refactor: Rephrase test and associated comments In the end, the issue was not the seeding/regularization of the Hessian per se, but (primarily) a Hessian indexing error that occured when some parameters were fixed and/or limit-constrained (see fix commit a516d01e65b7f215357dad380fb46ad978e745fe). --- .../Any_gradient_based_minimizer.spec.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/minuit2.net.UnitTests/Any_gradient_based_minimizer.spec.cs b/test/minuit2.net.UnitTests/Any_gradient_based_minimizer.spec.cs index 6c82e75..670518e 100644 --- a/test/minuit2.net.UnitTests/Any_gradient_based_minimizer.spec.cs +++ b/test/minuit2.net.UnitTests/Any_gradient_based_minimizer.spec.cs @@ -193,15 +193,10 @@ public void when_minimizing_a_cost_function_with_an_analytical_hessian_that_thro action.Should().ThrowExactly(); } - [Test, - Description("This test ensures the Hessian (diagonal) is regularized during minimizer seeding to prevent the " + - "minimizer from initially stepping away from the minimum (and eventually failing).")] - public void when_minimizing_a_cost_function_with_an_analytical_hessian_that_is_not_positive_definite_for_the_initial_parameter_values_yields_a_result_matching_the_result_obtained_for_numerical_approximation() + [Test] + public void when_minimizing_a_cost_function_with_an_analytical_hessian_that_is_not_positive_definite_for_the_initial_parameter_values_and_some_parameters_are_limited_yields_a_result_matching_the_result_obtained_for_numerical_approximation() { - // For the initial parameter values [2, 1, 0], the Hessian is not positive definite. Consequently, the initial - // Newton step points in the wrong direction — away from the local minimum. To prevent this, the initial - // Hessian (or its diagonal approximation) must be regularized to ensure positive definiteness during minimizer - // seeding. Without this safeguard, the minimizer will fail in this case (cf. https://github.com/root-project/root/issues/20665). + // For the initial parameter values [2, 1, 0], the Hessian is not positive definite. var problem = new ExponentialDecayProblem(); var parameterConfigurations = problem.ParameterConfigurations .WithParameter(1).WithLimits(0, null) From 68cdc75fc9aac2a31d0f40f83ffa2bb8b8445523 Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 21:17:21 +0100 Subject: [PATCH 5/6] refactor: Remove redundant ContinueOnError attributes false is the default for Exec tasks, so there is no need to specify it. --- Build.targets | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Build.targets b/Build.targets index d64b1f6..5432244 100644 --- a/Build.targets +++ b/Build.targets @@ -14,7 +14,6 @@ @@ -22,23 +21,19 @@ + WorkingDirectory="$(RootGitPath)"/> + WorkingDirectory="$(RootGitPath)"/> + WorkingDirectory="$(RootGitPath)"/> + WorkingDirectory="$(RootGitPath)"/> @@ -60,7 +55,6 @@ @@ -71,8 +65,7 @@ + WorkingDirectory="$(MSBuildThisFileDirectory)"/> From 68c56159f13478ecb05957f74f4ab8814e585998 Mon Sep 17 00:00:00 2001 From: Adrian Walser Date: Thu, 22 Jan 2026 21:23:32 +0100 Subject: [PATCH 6/6] refactor: Consistently use one line per attribute --- Build.targets | 63 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/Build.targets b/Build.targets index 5432244..b288527 100644 --- a/Build.targets +++ b/Build.targets @@ -7,17 +7,22 @@ - + - + - + - + - + x64 Win32 @@ -59,7 +67,9 @@ - + cmake --build "$(CmakeBuildDir)" --config Release @@ -68,7 +78,9 @@ WorkingDirectory="$(MSBuildThisFileDirectory)"/> - + @@ -76,13 +88,16 @@ - + - + - + PreserveNewest PreserveNewest false @@ -90,11 +105,19 @@ - + - - - + + + - + x64;x86;ARM64 @@ -118,7 +142,8 @@ - + - + - +