From 03ef7cbd01f80d13ccf78d7fad03e7fbe57328c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:19:42 +0100 Subject: [PATCH 1/7] test(optimizers): cover LIONsolver readiness checks Tests for the return codes of check_*_ready/check_complete, the argument mismatch in check_complete's call to check_validation_ready, and the CPU device fallback. Currently failing. Co-Authored-By: Claude Opus 5 --- tests/optimizers/test_solver_checks.py | 129 +++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/optimizers/test_solver_checks.py diff --git a/tests/optimizers/test_solver_checks.py b/tests/optimizers/test_solver_checks.py new file mode 100644 index 00000000..2e92771e --- /dev/null +++ b/tests/optimizers/test_solver_checks.py @@ -0,0 +1,129 @@ +"""Tests for the LIONsolver readiness checks. + +These cover two bugs in ``check_*_ready``/``check_complete``: + +- each check overwrote ``return_code`` instead of accumulating it, so the value + returned reflected only the *last* check. Callers act on that value (e.g. + ``LIONsolver.validate`` runs validation when ``check_validation_ready()`` is 0), + so a missing validation loader was reported as "ready". +- ``check_complete`` called ``check_validation_ready(error, autofill)``, but that + method's signature is ``(autofill, verbose)``, so the arguments landed in the + wrong parameters: ``verbose`` was given the value of ``autofill``. + +DnCNN is used because it needs no CT geometry of its own, so these run on CPU. +""" + +import sys +from unittest.mock import MagicMock + +import pytest +import torch +from torch.utils.data import DataLoader, TensorDataset + +# tomosipo needs a CUDA-enabled astra build and fails to import on some platforms +# (e.g. macOS). The solver only builds an operator with it, so stub it when the +# real one is missing. +try: + import tomosipo # noqa: F401 +except ImportError: + sys.modules["tomosipo"] = MagicMock() + sys.modules["tomosipo.torch_support"] = MagicMock() + +from LION.CTtools.ct_geometry import Geometry +from LION.models.CNNs.dncnn import DnCNN +from LION.optimizers.SupervisedSolver import SupervisedSolver + +# The checks warn about whatever is not set; that is the behaviour under test. +pytestmark = pytest.mark.filterwarnings("ignore::UserWarning") + + +def _loader(): + data = torch.zeros(2, 1, 4, 4) + return DataLoader(TensorDataset(data, data), batch_size=1) + + +def _solver(**kwargs): + model = DnCNN() + return SupervisedSolver( + model, + torch.optim.SGD(model.parameters(), lr=0.1), + torch.nn.MSELoss(), + geometry=Geometry.default_parameters(), + device=torch.device("cpu"), + **kwargs, + ) + + +def _ready_solver(tmp_path): + """A solver with training, validation, testing and saving all configured.""" + solver = _solver() + solver.set_training(_loader()) + solver.set_saving(tmp_path, "final_result.pt") + solver.set_validation(_loader(), 1) + solver.set_testing(_loader()) + return solver + + +def test_fully_configured_solver_reports_ready(tmp_path): + solver = _ready_solver(tmp_path) + assert solver.check_training_ready() == 0 + assert solver.check_validation_ready() == 0 + assert solver.check_testing_ready() == 0 + assert solver.check_complete() == 0 + + +def test_check_validation_ready_reports_missing_loader(tmp_path): + solver = _ready_solver(tmp_path) + solver.validation_loader = None + + # Must not be reported as ready: LIONsolver.validate() runs validation when + # this returns 0, which would then fail on the missing loader. + assert solver.check_validation_ready() != 0 + + +def test_check_training_ready_reports_failure_from_an_earlier_check(tmp_path): + solver = _ready_solver(tmp_path) + solver.train_loader = None # fails an early check; later checks still pass + + assert solver.check_training_ready(error=False) != 0 + + +def test_check_testing_ready_reports_failure_from_an_earlier_check(tmp_path): + solver = _ready_solver(tmp_path) + solver.test_loader = None # the testing_fn check after it still passes + + assert solver.check_testing_ready(error=False) != 0 + + +def test_check_complete_reports_failure_from_an_earlier_check(tmp_path): + solver = _ready_solver(tmp_path) + solver.test_loader = None # checkpointing is checked last and autofills + + assert solver.check_complete(error=False) != 0 + + +def test_check_complete_reports_bad_validation_fn_when_not_autofilling(tmp_path): + solver = _ready_solver(tmp_path) + solver.validation_fn = 42 # not callable + solver.verbose = True + + # check_complete must pass its own `verbose` to check_validation_ready. The + # old call passed `autofill` there, so with autofill=False the check went + # silent -- and __check_attribute only returns a code when it warns, so the + # bad validation_fn was hidden from the return value as well. + with pytest.warns(UserWarning, match="validation_fn"): + assert solver.check_complete(error=False, autofill=False) != 0 + + +@pytest.mark.skipif( + torch.cuda.is_available(), reason="checks the fallback when CUDA is absent" +) +def test_solver_defaults_to_cpu_without_cuda(): + model = DnCNN() + solver = SupervisedSolver( + model, + torch.optim.SGD(model.parameters(), lr=0.1), + torch.nn.MSELoss(), + geometry=Geometry.default_parameters(), + ) + assert solver.device == torch.device("cpu") From 4226a8e7a20e724239519ba6f2b46a1c74e77399 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:26:41 +0100 Subject: [PATCH 2/7] test(optimizers): cover checkpointing check and isolate from mocks Assert check_checkpointing_ready passes for a configured solver, and drop mock LION modules left in sys.modules by other test files so this module imports the real ones regardless of test order. Co-Authored-By: Claude Opus 5 --- tests/optimizers/test_solver_checks.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/optimizers/test_solver_checks.py b/tests/optimizers/test_solver_checks.py index 2e92771e..5227cf48 100644 --- a/tests/optimizers/test_solver_checks.py +++ b/tests/optimizers/test_solver_checks.py @@ -29,6 +29,13 @@ sys.modules["tomosipo"] = MagicMock() sys.modules["tomosipo.torch_support"] = MagicMock() +# Other test modules (e.g. tests/classical_algorithms/test_sirt.py) replace LION +# modules in sys.modules with mocks for the whole process, which breaks the real +# imports below depending on test order. Drop those fakes; real modules are kept. +for _name, _module in list(sys.modules.items()): + if _name.startswith("LION") and isinstance(_module, MagicMock): + del sys.modules[_name] + from LION.CTtools.ct_geometry import Geometry from LION.models.CNNs.dncnn import DnCNN from LION.optimizers.SupervisedSolver import SupervisedSolver @@ -69,6 +76,9 @@ def test_fully_configured_solver_reports_ready(tmp_path): assert solver.check_training_ready() == 0 assert solver.check_validation_ready() == 0 assert solver.check_testing_ready() == 0 + # checked the solver's `load_checkpoint` method instead of the + # `do_load_checkpoint` flag, so this always reported a type failure + assert solver.check_checkpointing_ready() == 0 assert solver.check_complete() == 0 From f844f9eee43c8b9f7c20dce30d4e5f58bb945455 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:26:41 +0100 Subject: [PATCH 3/7] fix(optimizers): stop solver readiness checks hiding failures Accumulate return codes across checks instead of overwriting them, return a code regardless of verbosity, pass autofill/verbose correctly from check_complete, check the do_load_checkpoint flag rather than the load_checkpoint method, and fall back to CPU when CUDA is unavailable. Co-Authored-By: Claude Opus 5 --- LION/optimizers/LIONsolver.py | 314 ++++++++++++++++++++-------------- 1 file changed, 186 insertions(+), 128 deletions(-) diff --git a/LION/optimizers/LIONsolver.py b/LION/optimizers/LIONsolver.py index 5d6832f1..e41ed597 100644 --- a/LION/optimizers/LIONsolver.py +++ b/LION/optimizers/LIONsolver.py @@ -93,7 +93,9 @@ def __init__( self.loss_fn = loss_fn if device is None: - device = torch.device(torch.cuda.current_device()) + device = torch.device( + torch.cuda.current_device() if torch.cuda.is_available() else "cpu" + ) self.device = device self.model.to(self.device) @@ -230,60 +232,78 @@ def check_training_ready(self, error=True, autofill=True, verbose=True): return_code = 0 # Test 1: is the device set? if not, set it if autofill is True - return_code = self.__check_attribute( - "device", - expected_type=torch.device, - error=False, - autofill=autofill, - verbose=verbose, - default=torch.device( - torch.cuda.current_device() if torch.cuda.is_available() else "cpu" + return_code = max( + return_code, + self.__check_attribute( + "device", + expected_type=torch.device, + error=False, + autofill=autofill, + verbose=verbose, + default=torch.device( + torch.cuda.current_device() if torch.cuda.is_available() else "cpu" + ), ), ) # Test 2: is the model set? if not, raise error or warn - return_code = self.__check_attribute( - "model", - expected_type=LIONmodel, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "model", + expected_type=LIONmodel, + error=error, + autofill=False, + verbose=verbose, + ), ) # Test 3: is the optimizer set? if not, raise error or warn - return_code = self.__check_attribute( - "optimizer", - expected_type=Optimizer, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "optimizer", + expected_type=Optimizer, + error=error, + autofill=False, + verbose=verbose, + ), ) # Test 4: is the loss_fn set? if not, raise error or warn - return_code = self.__check_attribute( - "loss_fn", - expected_type=callable, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "loss_fn", + expected_type=callable, + error=error, + autofill=False, + verbose=verbose, + ), ) # Test 7: is the training loader set? if not, raise error or warn - return_code = self.__check_attribute( - "train_loader", - expected_type=DataLoader, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "train_loader", + expected_type=DataLoader, + error=error, + autofill=False, + verbose=verbose, + ), ) # Test 12: is the final result filename set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "final_result_fname", - expected_type=str, - error=False, - autofill=False, - verbose=True, + return_code = max( + return_code, + self.__check_attribute( + "final_result_fname", + expected_type=str, + error=False, + autofill=False, + verbose=True, + ), ) return return_code @@ -292,40 +312,52 @@ def check_validation_ready(self, autofill=True, verbose=True): return_code = 0 # Test 8: is the validation loader set? if not, raise error or warn - return_code = self.__check_attribute( - "validation_loader", - expected_type=DataLoader, - error=False, - autofill=False, - verbose=True, + return_code = max( + return_code, + self.__check_attribute( + "validation_loader", + expected_type=DataLoader, + error=False, + autofill=False, + verbose=True, + ), ) # Test 9: is the validation function set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "validation_fn", - expected_type=callable, - error=False, - autofill=self.validation_loader is not None, - verbose=verbose, - default=self.loss_fn, + return_code = max( + return_code, + self.__check_attribute( + "validation_fn", + expected_type=callable, + error=False, + autofill=self.validation_loader is not None, + verbose=verbose, + default=self.loss_fn, + ), ) # Test 10: is the validation frequency set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "validation_freq", - expected_type=int, - error=False, - autofill=self.validation_loader is not None, - verbose=verbose, - default=10, + return_code = max( + return_code, + self.__check_attribute( + "validation_freq", + expected_type=int, + error=False, + autofill=self.validation_loader is not None, + verbose=verbose, + default=10, + ), ) - return_code = self.__check_attribute( - "validation_save_folder", - expected_type=pathlib.Path, - error=False, - autofill=True, - verbose=True, - default=self.save_folder, + return_code = max( + return_code, + self.__check_attribute( + "validation_save_folder", + expected_type=pathlib.Path, + error=False, + autofill=True, + verbose=True, + default=self.save_folder, + ), ) # Test 14: is the validation filename set? if not, raise error or warn or autofill @@ -336,13 +368,16 @@ def check_validation_ready(self, autofill=True, verbose=True): default_validation_fname = f"{self.final_result_fname}_min_val.pt" else: default_validation_fname = None - return_code = self.__check_attribute( - "validation_fname", - expected_type=str, - error=False, - autofill=True, - verbose=False, - default=default_validation_fname, + return_code = max( + return_code, + self.__check_attribute( + "validation_fname", + expected_type=str, + error=False, + autofill=True, + verbose=False, + default=default_validation_fname, + ), ) return return_code @@ -351,20 +386,26 @@ def check_testing_ready(self, error=True, verbose=True): return_code = 0 # Test 5: is the testing loader set? if not, raise error or warn - return_code = self.__check_attribute( - "test_loader", - expected_type=DataLoader, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "test_loader", + expected_type=DataLoader, + error=error, + autofill=False, + verbose=verbose, + ), ) # Test 6: is the testing function set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "testing_fn", - expected_type=callable, - error=error, - autofill=False, - verbose=verbose, + return_code = max( + return_code, + self.__check_attribute( + "testing_fn", + expected_type=callable, + error=error, + autofill=False, + verbose=verbose, + ), ) return return_code @@ -372,13 +413,16 @@ def check_testing_ready(self, error=True, verbose=True): def check_checkpointing_ready(self, autofill=True, verbose=True): return_code = 0 - return_code = self.__check_attribute( - "checkpoint_save_folder", - expected_type=pathlib.Path, - error=False, - autofill=True, - verbose=True, - default=self.save_folder, + return_code = max( + return_code, + self.__check_attribute( + "checkpoint_save_folder", + expected_type=pathlib.Path, + error=False, + autofill=True, + verbose=True, + default=self.save_folder, + ), ) # Test 13: is the checkpoint filename filename set? if not, raise error or warn or autofill @@ -389,33 +433,42 @@ def check_checkpointing_ready(self, autofill=True, verbose=True): default_checkpoint_fname = f"{self.final_result_fname}_checkpoint_*.pt" else: default_checkpoint_fname = None - return_code = self.__check_attribute( - "checkpoint_fname", - expected_type=str, - error=False, - autofill=True, - verbose=False, - default=default_checkpoint_fname, + return_code = max( + return_code, + self.__check_attribute( + "checkpoint_fname", + expected_type=str, + error=False, + autofill=True, + verbose=False, + default=default_checkpoint_fname, + ), ) # Test 15: is the checkpoint frequency set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "checkpoint_freq", - expected_type=int, - error=False, - autofill=autofill, - verbose=verbose, - default=10, + return_code = max( + return_code, + self.__check_attribute( + "checkpoint_freq", + expected_type=int, + error=False, + autofill=autofill, + verbose=verbose, + default=10, + ), ) # Test 16: is the load checkpoint set? if not, raise error or warn or autofill - return_code = self.__check_attribute( - "load_checkpoint", - expected_type=bool, - error=False, - autofill=True, - verbose=verbose, - default=True, + return_code = max( + return_code, + self.__check_attribute( + "do_load_checkpoint", + expected_type=bool, + error=False, + autofill=True, + verbose=verbose, + default=True, + ), ) return return_code @@ -445,15 +498,19 @@ def check_complete(self, error=True, autofill=True): verbose = True self.verbose = verbose - return_code = self.check_training_ready(error, autofill, verbose) + return_code = max( + return_code, self.check_training_ready(error, autofill, verbose) + ) - return_code = self.check_testing_ready(error, verbose) + return_code = max(return_code, self.check_testing_ready(error, verbose)) - return_code = self.check_saving_ready() + return_code = max(return_code, self.check_saving_ready()) - return_code = self.check_validation_ready(error, autofill) + return_code = max(return_code, self.check_validation_ready(autofill, verbose)) - return_code = self.check_checkpointing_ready(autofill, verbose) + return_code = max( + return_code, self.check_checkpointing_ready(autofill, verbose) + ) self.verbose = verbose @@ -480,29 +537,30 @@ def __check_attribute( else: if error: raise ValueError(f"Attribute {attr} not set") - elif verbose: + if verbose: warnings.warn(f"Attribute {attr} not set") - return 1 + # the return code must not depend on verbosity + return 1 # if the type we want to check against is a function, we need to treat it differently if expected_type is callable: if not callable(getattr(self, attr)): if error: raise ValueError(f"Attribute {attr} is not callable") - elif verbose: + if verbose: warnings.warn(f"Attribute {attr} is not callable") - return 2 + return 2 # just standrad type chekcking, error or warn, depends of settings elif not isinstance(getattr(self, attr), expected_type): if error: raise ValueError( f"Attribute {attr} is not of type {expected_type}, its {type(getattr(self, attr))}" ) - elif verbose: + if verbose: warnings.warn( f"Attribute {attr} is not of type {expected_type}, its {type(getattr(self, attr))}" ) - return 2 + return 2 return 0 def save_checkpoint(self, epoch): @@ -640,10 +698,10 @@ def load_checkpoint(self): and self.validation_fname is not None and self.validation_loss is not None ): - self.validation_loss[ - self.current_epoch - 1 - ] = self.model._read_min_validation( - self.checkpoint_save_folder.joinpath(self.validation_fname) + self.validation_loss[self.current_epoch - 1] = ( + self.model._read_min_validation( + self.checkpoint_save_folder.joinpath(self.validation_fname) + ) ) if self.verbose: print( From 12d2c24968e8a3286d94718961ec5ea5adef5e20 Mon Sep 17 00:00:00 2001 From: Tim Leach <4410524+TimLeach635@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:55:47 +0100 Subject: [PATCH 4/7] docs: remove overly verbose AI comments --- tests/optimizers/test_solver_checks.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/optimizers/test_solver_checks.py b/tests/optimizers/test_solver_checks.py index 5227cf48..ec647364 100644 --- a/tests/optimizers/test_solver_checks.py +++ b/tests/optimizers/test_solver_checks.py @@ -1,15 +1,5 @@ """Tests for the LIONsolver readiness checks. -These cover two bugs in ``check_*_ready``/``check_complete``: - -- each check overwrote ``return_code`` instead of accumulating it, so the value - returned reflected only the *last* check. Callers act on that value (e.g. - ``LIONsolver.validate`` runs validation when ``check_validation_ready()`` is 0), - so a missing validation loader was reported as "ready". -- ``check_complete`` called ``check_validation_ready(error, autofill)``, but that - method's signature is ``(autofill, verbose)``, so the arguments landed in the - wrong parameters: ``verbose`` was given the value of ``autofill``. - DnCNN is used because it needs no CT geometry of its own, so these run on CPU. """ @@ -117,10 +107,7 @@ def test_check_complete_reports_bad_validation_fn_when_not_autofilling(tmp_path) solver.validation_fn = 42 # not callable solver.verbose = True - # check_complete must pass its own `verbose` to check_validation_ready. The - # old call passed `autofill` there, so with autofill=False the check went - # silent -- and __check_attribute only returns a code when it warns, so the - # bad validation_fn was hidden from the return value as well. + # check_complete must pass its own `verbose` to check_validation_ready with pytest.warns(UserWarning, match="validation_fn"): assert solver.check_complete(error=False, autofill=False) != 0 From 02c97c26ee6e5ba80df5d220ac2898198dce454e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 15:33:44 +0100 Subject: [PATCH 5/7] docs(optimizers): trim comments in solver check tests Drop commentary that described past behaviour and shorten the rest. Co-Authored-By: Claude Opus 5 --- tests/optimizers/test_solver_checks.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/optimizers/test_solver_checks.py b/tests/optimizers/test_solver_checks.py index ec647364..bb6cb368 100644 --- a/tests/optimizers/test_solver_checks.py +++ b/tests/optimizers/test_solver_checks.py @@ -10,18 +10,15 @@ import torch from torch.utils.data import DataLoader, TensorDataset -# tomosipo needs a CUDA-enabled astra build and fails to import on some platforms -# (e.g. macOS). The solver only builds an operator with it, so stub it when the -# real one is missing. +# tomosipo needs a CUDA-enabled astra build, unavailable on some platforms. try: import tomosipo # noqa: F401 except ImportError: sys.modules["tomosipo"] = MagicMock() sys.modules["tomosipo.torch_support"] = MagicMock() -# Other test modules (e.g. tests/classical_algorithms/test_sirt.py) replace LION -# modules in sys.modules with mocks for the whole process, which breaks the real -# imports below depending on test order. Drop those fakes; real modules are kept. +# Other test modules mock LION modules in sys.modules for the whole process, so +# drop those fakes to keep the imports below order-independent. for _name, _module in list(sys.modules.items()): if _name.startswith("LION") and isinstance(_module, MagicMock): del sys.modules[_name] @@ -66,8 +63,6 @@ def test_fully_configured_solver_reports_ready(tmp_path): assert solver.check_training_ready() == 0 assert solver.check_validation_ready() == 0 assert solver.check_testing_ready() == 0 - # checked the solver's `load_checkpoint` method instead of the - # `do_load_checkpoint` flag, so this always reported a type failure assert solver.check_checkpointing_ready() == 0 assert solver.check_complete() == 0 @@ -76,8 +71,7 @@ def test_check_validation_ready_reports_missing_loader(tmp_path): solver = _ready_solver(tmp_path) solver.validation_loader = None - # Must not be reported as ready: LIONsolver.validate() runs validation when - # this returns 0, which would then fail on the missing loader. + # callers run validation when this returns 0 assert solver.check_validation_ready() != 0 From b59ca76a1beda731a8b2b0b3001472cfecce4128 Mon Sep 17 00:00:00 2001 From: Tim Leach <4410524+TimLeach635@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:05:55 +0100 Subject: [PATCH 6/7] format: placate black --- LION/optimizers/LIONsolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LION/optimizers/LIONsolver.py b/LION/optimizers/LIONsolver.py index e41ed597..d72b0a14 100644 --- a/LION/optimizers/LIONsolver.py +++ b/LION/optimizers/LIONsolver.py @@ -698,10 +698,10 @@ def load_checkpoint(self): and self.validation_fname is not None and self.validation_loss is not None ): - self.validation_loss[self.current_epoch - 1] = ( - self.model._read_min_validation( - self.checkpoint_save_folder.joinpath(self.validation_fname) - ) + self.validation_loss[ + self.current_epoch - 1 + ] = self.model._read_min_validation( + self.checkpoint_save_folder.joinpath(self.validation_fname) ) if self.verbose: print( From 523e63c9fb56a8bdfaea018ebcb7e6c23e49d6ed Mon Sep 17 00:00:00 2001 From: Tim Leach <4410524+TimLeach635@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:54:35 +0100 Subject: [PATCH 7/7] test: remove unnecessary tests --- tests/optimizers/test_solver_checks.py | 120 ------------------------- 1 file changed, 120 deletions(-) delete mode 100644 tests/optimizers/test_solver_checks.py diff --git a/tests/optimizers/test_solver_checks.py b/tests/optimizers/test_solver_checks.py deleted file mode 100644 index bb6cb368..00000000 --- a/tests/optimizers/test_solver_checks.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Tests for the LIONsolver readiness checks. - -DnCNN is used because it needs no CT geometry of its own, so these run on CPU. -""" - -import sys -from unittest.mock import MagicMock - -import pytest -import torch -from torch.utils.data import DataLoader, TensorDataset - -# tomosipo needs a CUDA-enabled astra build, unavailable on some platforms. -try: - import tomosipo # noqa: F401 -except ImportError: - sys.modules["tomosipo"] = MagicMock() - sys.modules["tomosipo.torch_support"] = MagicMock() - -# Other test modules mock LION modules in sys.modules for the whole process, so -# drop those fakes to keep the imports below order-independent. -for _name, _module in list(sys.modules.items()): - if _name.startswith("LION") and isinstance(_module, MagicMock): - del sys.modules[_name] - -from LION.CTtools.ct_geometry import Geometry -from LION.models.CNNs.dncnn import DnCNN -from LION.optimizers.SupervisedSolver import SupervisedSolver - -# The checks warn about whatever is not set; that is the behaviour under test. -pytestmark = pytest.mark.filterwarnings("ignore::UserWarning") - - -def _loader(): - data = torch.zeros(2, 1, 4, 4) - return DataLoader(TensorDataset(data, data), batch_size=1) - - -def _solver(**kwargs): - model = DnCNN() - return SupervisedSolver( - model, - torch.optim.SGD(model.parameters(), lr=0.1), - torch.nn.MSELoss(), - geometry=Geometry.default_parameters(), - device=torch.device("cpu"), - **kwargs, - ) - - -def _ready_solver(tmp_path): - """A solver with training, validation, testing and saving all configured.""" - solver = _solver() - solver.set_training(_loader()) - solver.set_saving(tmp_path, "final_result.pt") - solver.set_validation(_loader(), 1) - solver.set_testing(_loader()) - return solver - - -def test_fully_configured_solver_reports_ready(tmp_path): - solver = _ready_solver(tmp_path) - assert solver.check_training_ready() == 0 - assert solver.check_validation_ready() == 0 - assert solver.check_testing_ready() == 0 - assert solver.check_checkpointing_ready() == 0 - assert solver.check_complete() == 0 - - -def test_check_validation_ready_reports_missing_loader(tmp_path): - solver = _ready_solver(tmp_path) - solver.validation_loader = None - - # callers run validation when this returns 0 - assert solver.check_validation_ready() != 0 - - -def test_check_training_ready_reports_failure_from_an_earlier_check(tmp_path): - solver = _ready_solver(tmp_path) - solver.train_loader = None # fails an early check; later checks still pass - - assert solver.check_training_ready(error=False) != 0 - - -def test_check_testing_ready_reports_failure_from_an_earlier_check(tmp_path): - solver = _ready_solver(tmp_path) - solver.test_loader = None # the testing_fn check after it still passes - - assert solver.check_testing_ready(error=False) != 0 - - -def test_check_complete_reports_failure_from_an_earlier_check(tmp_path): - solver = _ready_solver(tmp_path) - solver.test_loader = None # checkpointing is checked last and autofills - - assert solver.check_complete(error=False) != 0 - - -def test_check_complete_reports_bad_validation_fn_when_not_autofilling(tmp_path): - solver = _ready_solver(tmp_path) - solver.validation_fn = 42 # not callable - solver.verbose = True - - # check_complete must pass its own `verbose` to check_validation_ready - with pytest.warns(UserWarning, match="validation_fn"): - assert solver.check_complete(error=False, autofill=False) != 0 - - -@pytest.mark.skipif( - torch.cuda.is_available(), reason="checks the fallback when CUDA is absent" -) -def test_solver_defaults_to_cpu_without_cuda(): - model = DnCNN() - solver = SupervisedSolver( - model, - torch.optim.SGD(model.parameters(), lr=0.1), - torch.nn.MSELoss(), - geometry=Geometry.default_parameters(), - ) - assert solver.device == torch.device("cpu")