From 9f6d60be2712accf5223810b4cb9e55558a3b036 Mon Sep 17 00:00:00 2001 From: Reece Boston <52183986+rboston628@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:04:34 -0500 Subject: [PATCH] Handle multiple reduction runs (#500) * get matching cal/norm versions * update workflow warnings * update code coverage --- src/snapred/ui/workflow/ReductionWorkflow.py | 38 +++++++++++-------- .../service/test_CalibrationService.py | 27 +++++++++++++ .../service/test_NormalizationService.py | 27 +++++++++++++ 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/snapred/ui/workflow/ReductionWorkflow.py b/src/snapred/ui/workflow/ReductionWorkflow.py index 15c433365..5f311e23d 100644 --- a/src/snapred/ui/workflow/ReductionWorkflow.py +++ b/src/snapred/ui/workflow/ReductionWorkflow.py @@ -189,7 +189,8 @@ def _triggerReduction(self, workflowPresenter): self._keeps.update(loadedCalibrations) self._keeps.update(loadedNormalizations) - if len(loadedNormalizations) > 1 and None in normVersions: + distinctNormVersions = set(normVersions.values()) + if len(distinctNormVersions) > 1 and None in distinctNormVersions: raise RuntimeError( "Some of your workspaces require Artificial Normalization. " "SNAPRed can currently only handle the situation where all, or none " @@ -197,27 +198,32 @@ def _triggerReduction(self, workflowPresenter): "and try again." ) - for runNumber in self.runNumbers: - self._artificialNormalizationView.updateRunNumber(runNumber) - request_ = self._createReductionRequest(runNumber) - - # Validate reduction; if artificial normalization is needed, handle it - response = self.request(path="reduction/validate", payload=request_) - if ContinueWarning.Type.MISSING_NORMALIZATION in self.continueAnywayFlags: + # Validate reduction; if artificial normalization is needed, handle it + # NOTE: this logic ONLY works because we are forbidding mixed cases of artnorm or loaded norm + response = self.request(path="reduction/validate", payload=request_) + if ContinueWarning.Type.MISSING_NORMALIZATION in self.continueAnywayFlags: + if len(self.runNumbers) > 1: + raise RuntimeError( + "Currently, Artificial Normalization can only be performed on a " + "single run at a time. Please clear your run list and try again." + ) + for runNumber in self.runNumbers: + self._artificialNormalizationView.updateRunNumber(runNumber) self._artificialNormalizationView.showAdjustView() + request_ = self._createReductionRequest(runNumber) response = self.request(path="reduction/grabWorkspaceforArtificialNorm", payload=request_) self._artificialNormalization(workflowPresenter, response.data, runNumber) - else: - # Proceed with reduction if artificial normalization is not needed + else: + for runNumber in self.runNumbers: self._artificialNormalizationView.showSkippedView() + request_ = self._createReductionRequest(runNumber) response = self.request(path="reduction/", payload=request_) if response.code == ResponseCode.OK: - record, unfocusedData = response.data.record, response.data.unfocusedData - self._finalizeReduction(record, unfocusedData) - # after each run, clean workspaces except groupings, calibrations, normalizations, and outputs - self._keeps.update(self.outputs) - self._clearWorkspaces(exclude=self._keeps, clearCachedWorkspaces=True) - workflowPresenter.advanceWorkflow() + self._finalizeReduction(response.data.record, response.data.unfocusedData) + # after each run, clean workspaces except groupings, calibrations, normalizations, and outputs + self._keeps.update(self.outputs) + self._clearWorkspaces(exclude=self._keeps, clearCachedWorkspaces=True) + workflowPresenter.advanceWorkflow() # SPECIAL FOR THE REDUCTION WORKFLOW: clear everything _except_ the output workspaces # _before_ transitioning to the "save" panel. # TODO: make '_clearWorkspaces' a public method (i.e make this combination a special `cleanup` method). diff --git a/tests/unit/backend/service/test_CalibrationService.py b/tests/unit/backend/service/test_CalibrationService.py index c4928604b..f246b7990 100644 --- a/tests/unit/backend/service/test_CalibrationService.py +++ b/tests/unit/backend/service/test_CalibrationService.py @@ -1043,6 +1043,33 @@ def test_fitPeaks(self, FitMultiplePeaksRecipe): res = self.instance.fitPeaks(request) assert res == FitMultiplePeaksRecipe.return_value.executeRecipe.return_value + def test_matchRuns(self): + self.instance.dataFactoryService.getThisOrLatestCalibrationVersion = mock.Mock( + side_effect=[mock.sentinel.version1, mock.sentinel.version2, mock.sentinel.version3], + ) + request = mock.Mock(runNumbers=[mock.sentinel.run1, mock.sentinel.run2], useLiteMode=True) + response = self.instance.matchRunsToCalibrationVersions(request) + assert response == { + mock.sentinel.run1: mock.sentinel.version1, + mock.sentinel.run2: mock.sentinel.version2, + } + + def test_fetchRuns(self): + mockCalibrations = { + mock.sentinel.run1: mock.sentinel.version1, + mock.sentinel.run2: mock.sentinel.version2, + mock.sentinel.run3: mock.sentinel.version2, + } + mockGroceries = [mock.sentinel.grocery1, mock.sentinel.grocery2, mock.sentinel.grocery2] + self.instance.matchRunsToCalibrationVersions = mock.Mock(return_value=mockCalibrations) + self.instance.groceryService.fetchGroceryList = mock.Mock(return_value=mockGroceries) + self.instance.groceryClerk = mock.Mock() + + request = mock.Mock(runNumbers=[mock.sentinel.run1, mock.sentinel.run2], useLiteMode=True) + groceries, cal = self.instance.fetchMatchingCalibrations(request) + assert groceries == {mock.sentinel.grocery1, mock.sentinel.grocery2} + assert cal == mockCalibrations + def test_initializeState(self): testCalibration = DAOFactory.calibrationParameters() mockInitializeState = mock.Mock(return_value=testCalibration.instrumentState) diff --git a/tests/unit/backend/service/test_NormalizationService.py b/tests/unit/backend/service/test_NormalizationService.py index 50222185b..7e9d4895d 100644 --- a/tests/unit/backend/service/test_NormalizationService.py +++ b/tests/unit/backend/service/test_NormalizationService.py @@ -207,6 +207,33 @@ def test_smoothDataExcludingPeaks( SmoothingParameter=mockRequest.smoothingParameter, ) + def test_matchRuns(self): + self.instance.dataFactoryService.getThisOrLatestNormalizationVersion = mock.Mock( + side_effect=[mock.sentinel.version1, mock.sentinel.version2], + ) + request = mock.Mock(runNumbers=[mock.sentinel.run1, mock.sentinel.run2], useLiteMode=True) + response = self.instance.matchRunsToNormalizationVersions(request) + assert response == { + mock.sentinel.run1: mock.sentinel.version1, + mock.sentinel.run2: mock.sentinel.version2, + } + + def test_fetchRuns(self): + mockCalibrations = { + mock.sentinel.run1: mock.sentinel.version1, + mock.sentinel.run2: mock.sentinel.version2, + mock.sentinel.run3: mock.sentinel.version2, + } + mockGroceries = [mock.sentinel.grocery1, mock.sentinel.grocery2, mock.sentinel.grocery2] + self.instance.matchRunsToNormalizationVersions = mock.Mock(return_value=mockCalibrations) + self.instance.groceryService.fetchGroceryList = mock.Mock(return_value=mockGroceries) + self.instance.groceryClerk = mock.Mock() + + request = mock.Mock(runNumbers=[mock.sentinel.run1, mock.sentinel.run2], useLiteMode=True) + groceries, cal = self.instance.fetchMatchingNormalizations(request) + assert groceries == {mock.sentinel.grocery1, mock.sentinel.grocery2} + assert cal == mockCalibrations + def test_normalizationAssessment(self): self.instance = NormalizationService() self.instance.sousChef = SculleryBoy()