Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions cpp/include/qdk/chemistry/algorithms/algorithm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,19 @@ class AlgorithmFactory {
* @return A unique pointer to the created algorithm instance.
* @throws std::runtime_error if the name is not found in the registry.
*/
static return_type create(const std::string& name = "") {
static return_type create(const std::string& name = "",
bool suppress_warnings = false) {
auto instance = create_impl(name);
if (!suppress_warnings) {
if (const auto message = detail::DeprecationAccess::message(*instance)) {
QDK_LOGGER().warn(*message);
}
}
return instance;
}

private:
static return_type create_impl(const std::string& name) {
std::string key = name;
if (key.empty()) {
key = Derived::default_algorithm_name();
Expand Down Expand Up @@ -289,12 +301,10 @@ class AlgorithmFactory {
"Algorithm factory for " + Derived::algorithm_type_name() +
": Algorithm with name '" + key + "' returned nullptr");
}
if (const auto message = detail::DeprecationAccess::message(*instance)) {
QDK_LOGGER().warn(*message);
}
return instance;
}

public:
/**
* @brief Register a new algorithm implementation.
*
Expand Down
13 changes: 9 additions & 4 deletions python/src/pybind11/algorithms/factory_bindings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,18 @@ See Also:
// Bind create static method
factory.def_static(
"create",
[](const std::string& name) -> std::unique_ptr<AlgorithmType> {
auto instance = FactoryType::create(name);
[](const std::string& name,
bool suppress_warnings) -> std::unique_ptr<AlgorithmType> {
auto instance = FactoryType::create(name, suppress_warnings);
if (!instance) {
throw std::runtime_error("Factory returned nullptr");
}
warn_if_deprecated_algorithm(*instance);
if (!suppress_warnings) {
warn_if_deprecated_algorithm(*instance);
}
return instance;
},
py::arg("name") = "", R"(
py::arg("name") = "", py::arg("suppress_warnings") = false, R"(
Create an algorithm instance by name.

If no name is provided or the name is empty, returns the default implementation.
Expand All @@ -128,6 +131,8 @@ If no name is provided or the name is empty, returns the default implementation.

If empty string (default), returns the default implementation.

suppress_warnings (bool): Whether to suppress creation-time warnings.

Returns:
Algorithm: New instance of the requested algorithm implementation

Expand Down
10 changes: 7 additions & 3 deletions python/src/qdk_chemistry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,9 @@ def _generate_registry_stubs() -> None:
for algorithm_type, algorithm_names in all_algorithms.items():
for algorithm_name in algorithm_names:
try:
settings = reg_module.inspect_settings(algorithm_type, algorithm_name)
instance = reg_module.create(algorithm_type, algorithm_name)
class_type = type(instance)
instance = reg_module.create(algorithm_type, algorithm_name, True)
settings = reg_module._inspect_instance_settings(instance) # noqa: SLF001
class_type = instance.__class__
class_name = class_type.__name__
class_module = class_type.__module__

Expand All @@ -349,8 +349,11 @@ def _generate_registry_stubs() -> None:
overload_lines.append("def create(")
overload_lines.append(f" algorithm_type: Literal['{algorithm_type}'],")
overload_lines.append(f" algorithm_name: Literal['{algorithm_name}'] | None = None,")
overload_lines.append(" *suppress_warnings: bool,")

for setting_name, setting_type, default, _, _ in settings:
if setting_name == "suppress_warnings":
continue
if setting_type == "str":
overload_lines.append(f' {setting_name}: {setting_type} = "{default}",')
elif "int" in setting_type:
Expand Down Expand Up @@ -387,6 +390,7 @@ def _generate_registry_stubs() -> None:
"def create(",
" algorithm_type: str,",
" algorithm_name: str | None = None,",
" *suppress_warnings: bool,",
" **kwargs,",
f") -> Union[{all_return_types_str}]: ...",
]
Expand Down
5 changes: 4 additions & 1 deletion python/src/qdk_chemistry/algorithms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def default_algorithm_name(self) -> str:

"""

def create(self, name: str | None = None) -> Algorithm:
def create(self, name: str | None = None, *, suppress_warnings: bool = False) -> Algorithm:
"""Create an algorithm instance by name.

Creates and returns a new instance of the requested algorithm. If no name
Expand All @@ -345,6 +345,8 @@ def create(self, name: str | None = None) -> Algorithm:

If None or empty, creates the default algorithm.

suppress_warnings (bool): Whether to suppress creation-time warnings.

Returns:
Algorithm: A new instance of the requested algorithm.

Expand All @@ -359,6 +361,7 @@ def create(self, name: str | None = None) -> Algorithm:
>>> pyscf_solver = factory.create("pyscf")

"""
del suppress_warnings
if name is None or name == "":
name = self.default_algorithm_name()
generator = self._registry.get(self._aliases.get(name, name))
Expand Down
45 changes: 33 additions & 12 deletions python/src/qdk_chemistry/algorithms/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,12 @@ def _resolve_algorithm_name(algorithm_type: str, algorithm_name: str) -> str:
return algorithm_name


def create(algorithm_type: str, algorithm_name: str | None = None, **kwargs) -> Algorithm:
def create(
algorithm_type: str,
algorithm_name: str | None = None,
*suppress_warnings: bool,
**kwargs: Any,
) -> Algorithm:
"""Create an algorithm instance by type and name.

This function creates an algorithm instance from the registry using the specified
Expand All @@ -335,6 +340,8 @@ def create(algorithm_type: str, algorithm_name: str | None = None, **kwargs) ->

If None or empty string, creates the default algorithm for that type.

suppress_warnings (bool): Positional-only flag that suppresses creation-time warnings.

kwargs: Optional keyword arguments (passed via ``**kwargs``).

These configure the algorithm's settings. These are forwarded directly to the algorithm's settings
Expand All @@ -361,14 +368,25 @@ def create(algorithm_type: str, algorithm_name: str | None = None, **kwargs) ->
>>> default_calc = registry.create("dynamical_correlation_calculator")

"""
if len(suppress_warnings) > 1:
raise TypeError(f"create() takes at most 3 positional arguments ({len(suppress_warnings) + 2} given)")
if "suppress_warnings" in kwargs:
raise TypeError(
"'suppress_warnings' is positional-only to avoid colliding with algorithm Settings. "
"Configure a setting with this name on the returned instance."
)
suppress_warnings_flag = suppress_warnings[0] if suppress_warnings else False
if not isinstance(suppress_warnings_flag, bool):
raise TypeError("'suppress_warnings' must be a bool")

algorithm_type = _resolve_algorithm_type(algorithm_type)
if algorithm_name is None:
algorithm_name = ""
algorithm_name = _resolve_algorithm_name(algorithm_type, algorithm_name)
for factory in __factories:
if factory.algorithm_type_name() == algorithm_type:
try:
instance = factory.create(algorithm_name)
instance = factory.create(algorithm_name, suppress_warnings=suppress_warnings_flag)
except (KeyError, RuntimeError, ValueError) as e:
available_algorithms = factory.available()
if not available_algorithms:
Expand Down Expand Up @@ -498,16 +516,7 @@ def inspect_settings(algorithm_type: str, algorithm_name: str) -> list[tuple[str
for factory in __factories:
if factory.algorithm_type_name() == algorithm_type:
instance = factory.create(algorithm_name)
settings = instance.settings().to_dict()
result = []
for name, default in settings.items():
python_type = instance.settings().get_expected_python_type(name)
description = (
instance.settings().get_description(name) if instance.settings().has_description(name) else None
)
limits = instance.settings().get_limits(name) if instance.settings().has_limits(name) else None
result.append((name, python_type, default, description, limits))
return result
return _inspect_instance_settings(instance)
available_types = [factory.algorithm_type_name() for factory in __factories]
raise KeyError(
f"Algorithm type '{algorithm_type}' is not registered. Available algorithm types: {', '.join(available_types)}"
Expand All @@ -516,6 +525,18 @@ def inspect_settings(algorithm_type: str, algorithm_name: str) -> list[tuple[str
)


def _inspect_instance_settings(instance: Algorithm) -> list[tuple[str, str, Any, str | None, Any | None]]:
"""Inspect settings for an existing algorithm instance."""
settings = instance.settings().to_dict()
result = []
for name, default in settings.items():
python_type = instance.settings().get_expected_python_type(name)
description = instance.settings().get_description(name) if instance.settings().has_description(name) else None
limits = instance.settings().get_limits(name) if instance.settings().has_limits(name) else None
result.append((name, python_type, default, description, limits))
return result


def register(generator: Callable[[], Algorithm]) -> None:
"""Register a custom algorithm implementation.

Expand Down
90 changes: 90 additions & 0 deletions python/tests/test_mp2_natural_orbital_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Regression tests for MP2 natural-orbital localizer deprecation warnings."""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import subprocess
import sys
import warnings

import pytest

DEPRECATION_MESSAGE = "MP2NaturalOrbitalLocalizer is deprecated"


def test_registry_stub_generation_does_not_warn_about_mp2_natural_orbital_localizer(tmp_path):
"""Registry stub generation does not report deprecated localizer use."""
stub_file = tmp_path / "registry.pyi"
stub_file.write_text("# placeholder\n", encoding="utf-8")

result = subprocess.run(
[
sys.executable,
"-W",
"always",
"-c",
"""\
import os
from pathlib import Path
import sys

os.environ["QDK_CHEMISTRY_DOCS"] = "1"

import qdk_chemistry
from qdk_chemistry.algorithms import registry
from qdk_chemistry.utils import Logger

stub_dir = Path(sys.argv[1])
registry.__file__ = str(stub_dir / "registry.py")
qdk_chemistry._STUBGEN_BLOCK_MARKER = stub_dir / ".no-stubgen"
Logger.set_global_level("warn")
qdk_chemistry._generate_registry_stubs()
""",
str(tmp_path),
],
capture_output=True,
check=False,
text=True,
)

assert result.returncode == 0, result.stderr
assert "Literal['qdk_mp2_natural_orbitals']" in stub_file.read_text(encoding="utf-8")
assert DEPRECATION_MESSAGE not in result.stdout
assert DEPRECATION_MESSAGE not in result.stderr


def test_explicit_mp2_natural_orbital_localizer_creation_warns_once(capfd):
"""Explicit registry creation retains its user-facing deprecation warning."""
from qdk_chemistry.algorithms import create # noqa: PLC0415
from qdk_chemistry.utils import Logger # noqa: PLC0415

previous_level = Logger.get_global_level()
try:
Logger.set_global_level("warn")
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
localizer = create("orbital_localizer", "qdk_mp2_natural_orbitals")
finally:
Logger.set_global_level(previous_level)

matching_warnings = [warning for warning in caught if DEPRECATION_MESSAGE in str(warning.message)]
captured = capfd.readouterr()

assert localizer.name() == "qdk_mp2_natural_orbitals"
assert len(matching_warnings) == 1
assert (captured.out + captured.err).count(DEPRECATION_MESSAGE) == 1


def test_creation_warning_control_is_positional_only():
"""The warning control cannot collide with an algorithm setting."""
from qdk_chemistry.algorithms import create # noqa: PLC0415

with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
localizer = create("orbital_localizer", "qdk_mp2_natural_orbitals", True)

assert localizer.name() == "qdk_mp2_natural_orbitals"
with pytest.raises(TypeError, match="positional-only"):
create("orbital_localizer", "qdk_mp2_natural_orbitals", suppress_warnings=True)
Loading