From 7e8c071110270cddc98a04f75255e69fd50df716 Mon Sep 17 00:00:00 2001 From: aochuba Date: Fri, 30 Jan 2026 18:59:57 +0530 Subject: [PATCH 1/4] Enhancement: Report projected gradient norm during MBIS optimization --- src/denspart/vh.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/denspart/vh.py b/src/denspart/vh.py index 8de7e9e..3224f57 100644 --- a/src/denspart/vh.py +++ b/src/denspart/vh.py @@ -111,6 +111,10 @@ def optimize_pro_model( print("#Iter #Call ekld kld -constraint grad.rms cputime (s)") print("----- ----- ----------- ----------- ----------- ----------- -----------") pars0 = np.concatenate([fn.pars for fn in pro_model.fns]) + # The errstate is changed to detect potentially nasty numerical issues. + # Optimize parameters within the bounds. + bounds = np.concatenate([fn.bounds for fn in pro_model.fns]) + cost_grad = partial( ekld, grid=grid, @@ -127,6 +131,14 @@ def callback(_current_pars, opt_result): # if info is None: # return gradient = info["gradient"] + # Compute projected gradient + grad_proj = gradient.copy() + lower = bounds[:, 0] + upper = bounds[:, 1] + mask_lower = (_current_pars <= lower + 1e-10) & (gradient > 0) + mask_upper = (_current_pars >= upper - 1e-10) & (gradient < 0) + grad_proj[mask_lower | mask_upper] = 0.0 + print( "{:5d} {:6d} {:12.7f} {:12.7f} {:12.4e} {:12.4e} {:12.7f}".format( opt_result.nit, @@ -134,8 +146,7 @@ def callback(_current_pars, opt_result): info["ekld"], info["kld"], -info["constraint"], - # TODO: projected gradient may be better. - np.linalg.norm(gradient) / np.sqrt(len(gradient)), + np.linalg.norm(grad_proj) / np.sqrt(len(grad_proj)), info["time"], ) ) @@ -145,10 +156,6 @@ def callback(_current_pars, opt_result): "Please report this issue on https://github.com/theochem/denspart/issues" ) - # The errstate is changed to detect potentially nasty numerical issues. - # Optimize parameters within the bounds. - bounds = np.concatenate([fn.bounds for fn in pro_model.fns]) - optresult = minimize( cost_grad, pars0, From 9439177e01ffd8e90f715b94d3f23736178a7a2d Mon Sep 17 00:00:00 2001 From: aochuba Date: Sun, 8 Feb 2026 00:59:33 +0530 Subject: [PATCH 2/4] Fix CI: Pin scipy<1.15 and update tests for compatibility --- pyproject.toml | 2 +- tests/test_properties.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4e391c..0a8a9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] -dependencies = ["numpy>=1.0", "scipy"] +dependencies = ["numpy>=1.0", "scipy<1.15"] dynamic = ["version"] [project.urls] diff --git a/tests/test_properties.py b/tests/test_properties.py index 04d7aba..dd5e2df 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -21,7 +21,13 @@ import numpy as np import pytest from numpy.testing import assert_allclose -from scipy.special import sph_harm_y +try: + from scipy.special import sph_harm_y +except ImportError: + from scipy.special import sph_harm + + def sph_harm_y(n, m, theta, phi): + return sph_harm(m, n, phi, theta) from denspart.properties import spherical_harmonics From 9f91dd66bccf1e958957a5090bfb1f7229b6fe64 Mon Sep 17 00:00:00 2001 From: aochuba Date: Mon, 9 Feb 2026 17:56:54 +0530 Subject: [PATCH 3/4] Revert scipy pinning and improve gradient projection robustness --- pyproject.toml | 2 +- src/denspart/vh.py | 31 +++++++++++++++++++++++++++++-- tests/test_properties.py | 8 +------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0a8a9c1..c4e391c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] -dependencies = ["numpy>=1.0", "scipy<1.15"] +dependencies = ["numpy>=1.0", "scipy"] dynamic = ["version"] [project.urls] diff --git a/src/denspart/vh.py b/src/denspart/vh.py index 3224f57..c8dfd4a 100644 --- a/src/denspart/vh.py +++ b/src/denspart/vh.py @@ -135,8 +135,35 @@ def callback(_current_pars, opt_result): grad_proj = gradient.copy() lower = bounds[:, 0] upper = bounds[:, 1] - mask_lower = (_current_pars <= lower + 1e-10) & (gradient > 0) - mask_upper = (_current_pars >= upper - 1e-10) & (gradient < 0) + # Identify parameters effectively at their bounds in a scale-aware way. + tol = 1e-8 + + # Check lower bounds + lower_finite = np.isfinite(lower) + scale_lower = np.ones_like(lower) + scale_lower[lower_finite] = np.maximum(1.0, np.abs(lower[lower_finite])) + at_lower = np.zeros_like(lower, dtype=bool) + at_lower[lower_finite] = np.isclose( + _current_pars[lower_finite], + lower[lower_finite], + rtol=tol, + atol=tol * scale_lower[lower_finite] + ) | (_current_pars[lower_finite] < lower[lower_finite]) + + # Check upper bounds + upper_finite = np.isfinite(upper) + scale_upper = np.ones_like(upper) + scale_upper[upper_finite] = np.maximum(1.0, np.abs(upper[upper_finite])) + at_upper = np.zeros_like(upper, dtype=bool) + at_upper[upper_finite] = np.isclose( + _current_pars[upper_finite], + upper[upper_finite], + rtol=tol, + atol=tol * scale_upper[upper_finite] + ) | (_current_pars[upper_finite] > upper[upper_finite]) + + mask_lower = at_lower & (gradient > 0) + mask_upper = at_upper & (gradient < 0) grad_proj[mask_lower | mask_upper] = 0.0 print( diff --git a/tests/test_properties.py b/tests/test_properties.py index dd5e2df..04d7aba 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -21,13 +21,7 @@ import numpy as np import pytest from numpy.testing import assert_allclose -try: - from scipy.special import sph_harm_y -except ImportError: - from scipy.special import sph_harm - - def sph_harm_y(n, m, theta, phi): - return sph_harm(m, n, phi, theta) +from scipy.special import sph_harm_y from denspart.properties import spherical_harmonics From f21caebe8b52488609d3adcf7ceb132f97d94bce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:34:17 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/denspart/vh.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/denspart/vh.py b/src/denspart/vh.py index c8dfd4a..1dd43a1 100644 --- a/src/denspart/vh.py +++ b/src/denspart/vh.py @@ -137,17 +137,17 @@ def callback(_current_pars, opt_result): upper = bounds[:, 1] # Identify parameters effectively at their bounds in a scale-aware way. tol = 1e-8 - + # Check lower bounds lower_finite = np.isfinite(lower) scale_lower = np.ones_like(lower) scale_lower[lower_finite] = np.maximum(1.0, np.abs(lower[lower_finite])) at_lower = np.zeros_like(lower, dtype=bool) at_lower[lower_finite] = np.isclose( - _current_pars[lower_finite], - lower[lower_finite], - rtol=tol, - atol=tol * scale_lower[lower_finite] + _current_pars[lower_finite], + lower[lower_finite], + rtol=tol, + atol=tol * scale_lower[lower_finite], ) | (_current_pars[lower_finite] < lower[lower_finite]) # Check upper bounds @@ -156,10 +156,10 @@ def callback(_current_pars, opt_result): scale_upper[upper_finite] = np.maximum(1.0, np.abs(upper[upper_finite])) at_upper = np.zeros_like(upper, dtype=bool) at_upper[upper_finite] = np.isclose( - _current_pars[upper_finite], - upper[upper_finite], - rtol=tol, - atol=tol * scale_upper[upper_finite] + _current_pars[upper_finite], + upper[upper_finite], + rtol=tol, + atol=tol * scale_upper[upper_finite], ) | (_current_pars[upper_finite] > upper[upper_finite]) mask_lower = at_lower & (gradient > 0)