diff --git a/src/snapred/backend/dao/ingredients/EffectiveInstrumentIngredients.py b/src/snapred/backend/dao/ingredients/EffectiveInstrumentIngredients.py index 356c70cb4..04cdef59f 100644 --- a/src/snapred/backend/dao/ingredients/EffectiveInstrumentIngredients.py +++ b/src/snapred/backend/dao/ingredients/EffectiveInstrumentIngredients.py @@ -4,7 +4,6 @@ class EffectiveInstrumentIngredients(BaseModel): - unmaskedPixelGroup: PixelGroup model_config = ConfigDict( diff --git a/src/snapred/backend/dao/ingredients/ReductionIngredients.py b/src/snapred/backend/dao/ingredients/ReductionIngredients.py index 93ff8377d..ea9ce4e4c 100644 --- a/src/snapred/backend/dao/ingredients/ReductionIngredients.py +++ b/src/snapred/backend/dao/ingredients/ReductionIngredients.py @@ -8,10 +8,10 @@ # These are from the same `__init__` module, so for the moment, we require the full import specifications. # (That is, not just "from snapred.backend.dao.ingredients import ...".) from snapred.backend.dao.ingredients.ArtificialNormalizationIngredients import ArtificialNormalizationIngredients +from snapred.backend.dao.ingredients.EffectiveInstrumentIngredients import EffectiveInstrumentIngredients from snapred.backend.dao.ingredients.GenerateFocussedVanadiumIngredients import GenerateFocussedVanadiumIngredients from snapred.backend.dao.ingredients.PreprocessReductionIngredients import PreprocessReductionIngredients from snapred.backend.dao.ingredients.ReductionGroupProcessingIngredients import ReductionGroupProcessingIngredients -from snapred.backend.dao.ingredients.EffectiveInstrumentIngredients import EffectiveInstrumentIngredients from snapred.backend.dao.state.PixelGroup import PixelGroup @@ -64,12 +64,10 @@ def applyNormalization(self, groupingIndex: int) -> ApplyNormalizationIngredient return ApplyNormalizationIngredients( pixelGroup=self.pixelGroups[groupingIndex], ) - + def effectiveInstrument(self, groupingIndex: int) -> EffectiveInstrumentIngredients: - return EffectiveInstrumentIngredients( - unmaskedPixelGroup=self.unmaskedPixelGroups[groupingIndex] - ) - + return EffectiveInstrumentIngredients(unmaskedPixelGroup=self.unmaskedPixelGroups[groupingIndex]) + model_config = ConfigDict( extra="forbid", ) diff --git a/src/snapred/backend/data/LocalDataService.py b/src/snapred/backend/data/LocalDataService.py index b8add744b..c7a14da1a 100644 --- a/src/snapred/backend/data/LocalDataService.py +++ b/src/snapred/backend/data/LocalDataService.py @@ -644,14 +644,14 @@ def writeReductionData(self, record: ReductionRecord): Persists the reduction data associated with a `ReductionRecord` -- `writeReductionRecord` must have been called prior to this method. """ - + # Implementation notes: # # 1) For SNAPRed's current reduction-workflow output implementation: # - # *`SaveNexusESS` _must_ be used, `SaveNexus` by itself won't work; + # *`SaveNexusESS` _must_ be used, `SaveNexus` by itself won't work; # - # * ONLY a simplified instrument geometry can be saved, + # * ONLY a simplified instrument geometry can be saved, # for example, as produced by `EditInstrumentGeometry`: # this geometry includes no monitors, only a single non-nested detector bank, and no parameter map. # @@ -660,7 +660,7 @@ def writeReductionData(self, record: ReductionRecord): # Hopefully this will eventually be fixed, but right now this is a limitation of Mantid's # instrument-I/O implementation (for non XML-based instruments). # - # 2) For SNAPRed internal use: + # 2) For SNAPRed internal use: # if `reduction.output.useEffectiveInstrument` is set to false in "application.yml", # output workspaces will be saved without converting their instruments to the reduced form. # This case is retained to allow some flexibility in what specifically is saved with the reduction data. @@ -677,26 +677,25 @@ def writeReductionData(self, record: ReductionRecord): raise RuntimeError(f"reduction version directories {filePath.parent} do not exist") useEffectiveInstrument = Config["reduction.output.useEffectiveInstrument"] - + for ws in record.workspaceNames: # Append workspaces to hdf5 file, in order of the `workspaces` list - + if ws.tokens("workspaceType") == wngt.REDUCTION_PIXEL_MASK: - # The mask workspace always uses the non-reduced instrument. self.mantidSnapper.SaveNexus( f"Append workspace '{ws}' to reduction output", InputWorkspace=ws, Filename=str(filePath), Append=True, - ) + ) self.mantidSnapper.executeQueue() - + # Write an additional copy of the combined pixel mask as a separate `SaveDiffCal`-format file maskFilename = ws + ".h5" self.writePixelMask(filePath.parent, Path(maskFilename), ws) else: - if useEffectiveInstrument: + if useEffectiveInstrument: self.mantidSnapper.SaveNexusESS( f"Append workspace '{ws}' to reduction output", InputWorkspace=ws, @@ -709,9 +708,9 @@ def writeReductionData(self, record: ReductionRecord): InputWorkspace=ws, Filename=str(filePath), Append=True, - ) + ) self.mantidSnapper.executeQueue() - + # Append the "metadata" group, containing the `ReductionRecord` metadata with h5py.File(filePath, "a") as h5: n5m.insertMetadataGroup(h5, record.dict(), "/metadata") diff --git a/src/snapred/backend/error/ContinueWarning.py b/src/snapred/backend/error/ContinueWarning.py index 7554d1d48..285c5487a 100644 --- a/src/snapred/backend/error/ContinueWarning.py +++ b/src/snapred/backend/error/ContinueWarning.py @@ -30,7 +30,7 @@ def flags(self): return self.model.flags def __init__(self, message: str, flags: "Type" = 0): - ContinueWarning.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method + ContinueWarning.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method self.model = ContinueWarning.Model(message=message, flags=flags) super().__init__(message) diff --git a/src/snapred/backend/error/RecoverableException.py b/src/snapred/backend/error/RecoverableException.py index 57007e7f9..a9f90c3c1 100644 --- a/src/snapred/backend/error/RecoverableException.py +++ b/src/snapred/backend/error/RecoverableException.py @@ -37,7 +37,7 @@ def data(self): return self.model.data def __init__(self, message: str, flags: "Type" = 0, data: Optional[Any] = None): - RecoverableException.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method + RecoverableException.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method self.model = RecoverableException.Model(message=message, flags=flags, data=data) logger.error(f"{extractTrueStacktrace()}") super().__init__(message) diff --git a/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py b/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py index 5e2ed7418..e8602f41a 100644 --- a/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py +++ b/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py @@ -1,4 +1,5 @@ from typing import Any, Dict, List, Tuple + import numpy as np from snapred.backend.dao.ingredients import EffectiveInstrumentIngredients as Ingredients @@ -11,14 +12,14 @@ logger = snapredLogger.getLogger(__name__) Pallet = Tuple[Ingredients, Dict[str, str]] - + + @Singleton class EffectiveInstrumentRecipe(Recipe[Ingredients]): - def unbagGroceries(self, groceries: Dict[str, Any]): self.inputWS = groceries["inputWorkspace"] self.outputWS = groceries.get("outputWorkspace", groceries["inputWorkspace"]) - + def chopIngredients(self, ingredients): self.unmaskedPixelGroup = ingredients.unmaskedPixelGroup @@ -30,9 +31,7 @@ def queueAlgos(self): # `EditInstrumentGeometry` modifies in-place, so we need to clone if a distinct output workspace is required. if self.outputWS != self.inputWS: self.mantidSnapper.CloneWorkspace( - "Clone workspace for reduced instrument", - OutputWorkspace=self.outputWS, - InputWorkspace=self.inputWS + "Clone workspace for reduced instrument", OutputWorkspace=self.outputWS, InputWorkspace=self.inputWS ) self.mantidSnapper.EditInstrumentGeometry( f"Editing instrument geometry for grouping '{self.unmaskedPixelGroup.focusGroup.name}'", @@ -42,7 +41,7 @@ def queueAlgos(self): Polar=np.rad2deg(self.unmaskedPixelGroup.twoTheta), Azimuthal=np.rad2deg(self.unmaskedPixelGroup.azimuth), # - InstrumentName=f"SNAP_{self.unmaskedPixelGroup.focusGroup.name}" + InstrumentName=f"SNAP_{self.unmaskedPixelGroup.focusGroup.name}", ) def validateInputs(self, ingredients: Ingredients, groceries: Dict[str, WorkspaceName]): diff --git a/src/snapred/backend/recipe/ReductionRecipe.py b/src/snapred/backend/recipe/ReductionRecipe.py index 657078bb3..aaf905d8b 100644 --- a/src/snapred/backend/recipe/ReductionRecipe.py +++ b/src/snapred/backend/recipe/ReductionRecipe.py @@ -3,15 +3,15 @@ from snapred.backend.dao.ingredients import ReductionIngredients as Ingredients from snapred.backend.log.logger import snapredLogger from snapred.backend.recipe.ApplyNormalizationRecipe import ApplyNormalizationRecipe +from snapred.backend.recipe.EffectiveInstrumentRecipe import EffectiveInstrumentRecipe from snapred.backend.recipe.GenerateFocussedVanadiumRecipe import GenerateFocussedVanadiumRecipe from snapred.backend.recipe.GenericRecipe import ArtificialNormalizationRecipe from snapred.backend.recipe.PreprocessReductionRecipe import PreprocessReductionRecipe -from snapred.backend.recipe.EffectiveInstrumentRecipe import EffectiveInstrumentRecipe from snapred.backend.recipe.Recipe import Recipe, WorkspaceName from snapred.backend.recipe.ReductionGroupProcessingRecipe import ReductionGroupProcessingRecipe +from snapred.meta.Config import Config from snapred.meta.mantid.WorkspaceNameGenerator import ValueFormatter as wnvf from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceNameGenerator as wng -from snapred.meta.Config import Config logger = snapredLogger.getLogger(__name__) @@ -283,7 +283,7 @@ def execute(self): self.ingredients.effectiveInstrument(groupingIndex), inputWorkspace=sampleClone, ) - + # Cleanup outputs.append(sampleClone) diff --git a/src/snapred/backend/service/ReductionService.py b/src/snapred/backend/service/ReductionService.py index 79a81fd70..89f2e1a96 100644 --- a/src/snapred/backend/service/ReductionService.py +++ b/src/snapred/backend/service/ReductionService.py @@ -1,7 +1,7 @@ -from typing import Any, Dict, List, Optional import json from collections.abc import Iterable from pathlib import Path +from typing import Any, Dict, List, Optional from snapred.backend.dao.ingredients import ( ArtificialNormalizationIngredients, @@ -308,7 +308,9 @@ def prepCombinedMask( return combinedMask @FromString - def prepReductionIngredients(self, request: ReductionRequest, combinedPixelMask: Optional[WorkspaceName] = None) -> ReductionIngredients: + def prepReductionIngredients( + self, request: ReductionRequest, combinedPixelMask: Optional[WorkspaceName] = None + ) -> ReductionIngredients: """ Prepare the needed ingredients for calculating reduction. Requires: @@ -387,7 +389,7 @@ def fetchReductionGroceries(self, request: ReductionRequest) -> Dict[str, Any]: raise RuntimeError( f"reduction pixel mask '{mask}' has unexpected workspace-type '{mask.tokens('workspaceType')}'" # noqa: E501 ) - if calVersion is not None: # WARNING: version may be _zero_! + if calVersion is not None: # WARNING: version may be _zero_! self.groceryClerk.name("diffcalMaskWorkspace").diffcal_mask(request.runNumber, calVersion).useLiteMode( request.useLiteMode ).add() @@ -409,7 +411,7 @@ def fetchReductionGroceries(self, request: ReductionRequest) -> Dict[str, Any]: request.useLiteMode ).add() - if normVersion is not None: # WARNING: version may be _zero_! + if normVersion is not None: # WARNING: version may be _zero_! self.groceryClerk.name("normalizationWorkspace").normalization(request.runNumber, normVersion).useLiteMode( request.useLiteMode ).add() diff --git a/src/snapred/backend/service/SousChef.py b/src/snapred/backend/service/SousChef.py index 61f152861..924254b85 100644 --- a/src/snapred/backend/service/SousChef.py +++ b/src/snapred/backend/service/SousChef.py @@ -1,7 +1,7 @@ -from typing import Dict, List, Optional, Tuple import os from copy import deepcopy from pathlib import Path +from typing import Dict, List, Optional, Tuple import pydantic @@ -29,9 +29,9 @@ from snapred.backend.recipe.PixelGroupingParametersCalculationRecipe import PixelGroupingParametersCalculationRecipe from snapred.backend.service.CrystallographicInfoService import CrystallographicInfoService from snapred.backend.service.Service import Service -from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceName from snapred.meta.Config import Config from snapred.meta.decorators.Singleton import Singleton +from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceName logger = snapredLogger.getLogger(__name__) @@ -95,7 +95,9 @@ def prepFocusGroup(self, ingredients: FarmFreshIngredients) -> FocusGroup: groupingMap = self.dataFactoryService.getGroupingMap(ingredients.runNumber) return groupingMap.getMap(ingredients.useLiteMode)[ingredients.focusGroup.name] - def prepPixelGroup(self, ingredients: FarmFreshIngredients, pixelMask: Optional[WorkspaceName] = None) -> PixelGroup: + def prepPixelGroup( + self, ingredients: FarmFreshIngredients, pixelMask: Optional[WorkspaceName] = None + ) -> PixelGroup: groupingSchema = ingredients.focusGroup.name key = (ingredients.runNumber, ingredients.useLiteMode, groupingSchema, pixelMask) if key not in self._pixelGroupCache: @@ -108,10 +110,7 @@ def prepPixelGroup(self, ingredients: FarmFreshIngredients, pixelMask: Optional[ self.groceryClerk.name("groupingWorkspace").fromRun(ingredients.runNumber).grouping( focusGroup.name ).useLiteMode(ingredients.useLiteMode).add() - groceries = self.groceryService.fetchGroceryDict( - self.groceryClerk.buildDict(), - maskWorkspace=pixelMask - ) + groceries = self.groceryService.fetchGroceryDict(self.groceryClerk.buildDict(), maskWorkspace=pixelMask) data = PixelGroupingParametersCalculationRecipe().executeRecipe(pixelIngredients, groceries) self._pixelGroupCache[key] = PixelGroup( @@ -122,7 +121,9 @@ def prepPixelGroup(self, ingredients: FarmFreshIngredients, pixelMask: Optional[ ) return deepcopy(self._pixelGroupCache[key]) - def prepManyPixelGroups(self, ingredients: FarmFreshIngredients, pixelMask: Optional[WorkspaceName] = None) -> List[PixelGroup]: + def prepManyPixelGroups( + self, ingredients: FarmFreshIngredients, pixelMask: Optional[WorkspaceName] = None + ) -> List[PixelGroup]: pixelGroups = [] ingredients_ = ingredients.model_copy() for focusGroup in ingredients.focusGroups: @@ -248,7 +249,9 @@ def _pullNormalizationRecordFFI( # TODO: Should smoothing parameter be an ingredient? return ingredients, smoothingParameter, calibrantSamplePath - def prepReductionIngredients(self, ingredients: FarmFreshIngredients, combinedPixelMask: Optional[WorkspaceName] = None) -> ReductionIngredients: + def prepReductionIngredients( + self, ingredients: FarmFreshIngredients, combinedPixelMask: Optional[WorkspaceName] = None + ) -> ReductionIngredients: ingredients_ = ingredients.model_copy() # some of the reduction ingredients MUST match those used in the calibration/normalization processes ingredients_ = self._pullCalibrationRecordFFI(ingredients_) diff --git a/src/snapred/resources/application.yml b/src/snapred/resources/application.yml index 8b8807fe3..33386a895 100644 --- a/src/snapred/resources/application.yml +++ b/src/snapred/resources/application.yml @@ -101,7 +101,7 @@ reduction: extension: .nxs # convert the instrument for the output workspaces into the reduced form useEffectiveInstrument: true - + mantid: workspace: nameTemplate: diff --git a/tests/resources/application.yml b/tests/resources/application.yml index 71730974e..532cf1962 100644 --- a/tests/resources/application.yml +++ b/tests/resources/application.yml @@ -108,7 +108,7 @@ reduction: extension: .nxs # convert the instrument for the output workspaces into the reduced form useEffectiveInstrument: true - + mantid: workspace: nameTemplate: diff --git a/tests/unit/backend/data/test_DataFactoryService.py b/tests/unit/backend/data/test_DataFactoryService.py index f8955e785..47d06ea85 100644 --- a/tests/unit/backend/data/test_DataFactoryService.py +++ b/tests/unit/backend/data/test_DataFactoryService.py @@ -1,7 +1,9 @@ import hashlib +import time +import unittest +import unittest.mock as mock from pathlib import Path from random import randint -import time from mantid.simpleapi import CreateSingleValuedWorkspace, DeleteWorkspace, mtd from snapred.backend.dao.calibration import Calibration @@ -13,8 +15,6 @@ from snapred.backend.data.DataFactoryService import DataFactoryService from snapred.backend.data.LocalDataService import LocalDataService -import unittest -import unittest.mock as mock class TestDataFactoryService(unittest.TestCase): def expected(cls, *args): diff --git a/tests/unit/backend/data/test_LocalDataService.py b/tests/unit/backend/data/test_LocalDataService.py index b153c9e95..facd6ba23 100644 --- a/tests/unit/backend/data/test_LocalDataService.py +++ b/tests/unit/backend/data/test_LocalDataService.py @@ -2,7 +2,6 @@ import importlib import json import logging -import numpy as np import os import re import socket @@ -16,6 +15,7 @@ from typing import List, Literal, Set import h5py +import numpy as np import pydantic import pytest from mantid.api import ITableWorkspace, MatrixWorkspace @@ -26,7 +26,6 @@ CompareWorkspaces, CreateGroupingWorkspace, CreateSampleWorkspace, - DeleteWorkspace, DeleteWorkspaces, EditInstrumentGeometry, GroupWorkspaces, @@ -1590,26 +1589,26 @@ def _createWorkspaces(wss: List[WorkspaceName]): Filename=fakeInstrumentFilePath, RewriteSpectraMap=True, ) - + # Mask workspace uses legacy instrument mask = mtd.unique_hidden_name() createCompatibleMask(mask, src) - + if Config["reduction.output.useEffectiveInstrument"]: # Convert the source workspace's instrument to the reduced form: # * no monitors; # * only one bank of detectors; # * no parameter map. - + detectorInfo = mtd[src].detectorInfo() l2s, twoThetas, azimuths = [], [], [] for n in range(detectorInfo.size()): if detectorInfo.isMonitor(n): continue - + l2 = detectorInfo.l2(n) twoTheta = detectorInfo.twoTheta(n) - + # See: defect EWM#7384 try: azimuth = detectorInfo.azimuthal(n) @@ -1622,17 +1621,14 @@ def _createWorkspaces(wss: List[WorkspaceName]): azimuths.append(azimuth) EditInstrumentGeometry( - Workspace=src, - L2=np.rad2deg(l2s), - Polar=np.rad2deg(twoThetas), - Azimuthal=np.rad2deg(azimuths) - ) + Workspace=src, L2=np.rad2deg(l2s), Polar=np.rad2deg(twoThetas), Azimuthal=np.rad2deg(azimuths) + ) assert mtd.doesExist(src) - + for ws in wss: CloneWorkspace( OutputWorkspace=ws, - InputWorkspace=src if ws.tokens("workspaceType") != wngt.REDUCTION_PIXEL_MASK else mask + InputWorkspace=src if ws.tokens("workspaceType") != wngt.REDUCTION_PIXEL_MASK else mask, ) assert mtd.doesExist(ws) cleanup_workspace_at_exit(ws) @@ -1671,13 +1667,13 @@ def test_writeReductionData(readSyntheticReductionRecord, createReductionWorkspa # `writeReductionRecord` must be called first localDataService.writeReductionRecord(testRecord) localDataService.writeReductionData(testRecord) - + assert reductionFilePath.exists() def test_writeReductionData_legacy_instrument(readSyntheticReductionRecord, createReductionWorkspaces): # Test that the special `Config` setting allows the saving of workspaces with non-reduced instruments - + # In order to facilitate parallel testing: any workspace name used by this test should be unique. inputRecordFilePath = Path(Resource.getPath("inputs/reduction/ReductionRecord_20240614T130420.json")) _uniqueTimestamp = 1731518208.172797 @@ -1797,7 +1793,7 @@ def test_readWriteReductionData(readSyntheticReductionRecord, createReductionWor filePath = reductionRecordFilePath.parent / fileName assert filePath.exists() - + # move the existing test workspaces out of the way: # * this just adds the `_uniquePrefix` one more time. RenameWorkspaces(InputWorkspaces=wss, Prefix=_uniquePrefix) @@ -1814,15 +1810,13 @@ def test_readWriteReductionData(readSyntheticReductionRecord, createReductionWor # please do _not_ replace this with one of the `assert_almost_equal` methods: # -- they do not necessarily do what you think they should do... for ws in actualRecord.workspaceNames: - equal, _ = CompareWorkspaces( - Workspace1=ws, - Workspace2=_uniquePrefix + ws, - CheckAllData=True - ) + equal, _ = CompareWorkspaces(Workspace1=ws, Workspace2=_uniquePrefix + ws, CheckAllData=True) assert equal -def test_readWriteReductionData_legacy_instrument(readSyntheticReductionRecord, createReductionWorkspaces, cleanup_workspace_at_exit): +def test_readWriteReductionData_legacy_instrument( + readSyntheticReductionRecord, createReductionWorkspaces, cleanup_workspace_at_exit +): # In order to facilitate parallel testing: any workspace name used by this test should be unique. _uniquePrefix = "_test_RWRD_" inputRecordFilePath = Path(Resource.getPath("inputs/reduction/ReductionRecord_20240614T130420.json")) @@ -1842,7 +1836,9 @@ def test_readWriteReductionData_legacy_instrument(readSyntheticReductionRecord, localDataService.getIPTS = mock.Mock(return_value="IPTS-12345") # Important to this test: use a path that doesn't already exist - reductionRecordFilePath = localDataService._constructReductionRecordFilePath(runNumber, useLiteMode, timestamp) + reductionRecordFilePath = localDataService._constructReductionRecordFilePath( + runNumber, useLiteMode, timestamp + ) assert not reductionRecordFilePath.exists() # `writeReductionRecord` needs to be called first @@ -1868,13 +1864,9 @@ def test_readWriteReductionData_legacy_instrument(readSyntheticReductionRecord, # please do _not_ replace this with one of the `assert_almost_equal` methods: # -- they do not necessarily do what you think they should do... for ws in actualRecord.workspaceNames: - equal, _ = CompareWorkspaces( - Workspace1=ws, - Workspace2=_uniquePrefix + ws, - CheckAllData=True - ) + equal, _ = CompareWorkspaces(Workspace1=ws, Workspace2=_uniquePrefix + ws, CheckAllData=True) assert equal - + def test_readWriteReductionData_pixel_mask( readSyntheticReductionRecord, createReductionWorkspaces, cleanup_workspace_at_exit diff --git a/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py b/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py index c4b1dba0f..e38ac5080 100644 --- a/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py +++ b/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py @@ -1,15 +1,15 @@ -import numpy as np +from unittest import mock -from snapred.backend.recipe.algorithm.Utensils import Utensils -from snapred.backend.recipe.EffectiveInstrumentRecipe import EffectiveInstrumentRecipe +import numpy as np +import pytest from snapred.backend.dao.ingredients import EffectiveInstrumentIngredients as Ingredients from snapred.backend.dao.state.FocusGroup import FocusGroup from snapred.backend.dao.state.PixelGroup import PixelGroup +from snapred.backend.recipe.algorithm.Utensils import Utensils +from snapred.backend.recipe.EffectiveInstrumentRecipe import EffectiveInstrumentRecipe from snapred.meta.Config import Resource from util.SculleryBoy import SculleryBoy -from unittest import mock -import pytest class TestEffectiveInstrumentRecipe: fakeInstrumentFilePath = Resource.getPath("inputs/testInstrument/fakeSNAP_Definition.xml") @@ -24,11 +24,8 @@ def _setup(self): L2=mock.Mock(), twoTheta=mock.Mock(), azimuth=mock.Mock(), - focusGroup=FocusGroup( - name="a_grouping", - definition="a/grouping/path" - ) - ) + focusGroup=FocusGroup(name="a_grouping", definition="a/grouping/path"), + ), ) self.ingredients1 = mock.Mock( spec=Ingredients, @@ -37,20 +34,16 @@ def _setup(self): L2=mock.Mock(), twoTheta=mock.Mock(), azimuth=mock.Mock(), - focusGroup=FocusGroup( - name="another_grouping", - definition="another/grouping/path" - ) - ) + focusGroup=FocusGroup(name="another_grouping", definition="another/grouping/path"), + ), ) self.ingredientss = [self.ingredients, self.ingredients1] - - + yield - + # teardown follows ... pass - + def test_chopIngredients(self): recipe = EffectiveInstrumentRecipe() ingredients = self.ingredients @@ -80,12 +73,12 @@ def test_queueAlgos(self): queuedAlgos = recipe.mantidSnapper._algorithmQueue - cloneWorkspaceTuple = queuedAlgos[0] + cloneWorkspaceTuple = queuedAlgos[0] assert cloneWorkspaceTuple[0] == "CloneWorkspace" assert cloneWorkspaceTuple[2]["InputWorkspace"] == groceries["inputWorkspace"] assert cloneWorkspaceTuple[2]["OutputWorkspace"] == groceries["outputWorkspace"] - - editInstrumentGeometryTuple = queuedAlgos[1] + + editInstrumentGeometryTuple = queuedAlgos[1] assert editInstrumentGeometryTuple[0] == "EditInstrumentGeometry" assert editInstrumentGeometryTuple[2]["Workspace"] == groceries["outputWorkspace"] @@ -98,7 +91,7 @@ def test_queueAlgos_default(self): queuedAlgos = recipe.mantidSnapper._algorithmQueue - editInstrumentGeometryTuple = queuedAlgos[0] + editInstrumentGeometryTuple = queuedAlgos[0] assert editInstrumentGeometryTuple[0] == "EditInstrumentGeometry" assert editInstrumentGeometryTuple[2]["Workspace"] == groceries["inputWorkspace"] @@ -118,7 +111,7 @@ def test_cook(self): mockSnapper.CloneWorkspace.assert_called_once_with( "Clone workspace for reduced instrument", OutputWorkspace=groceries["outputWorkspace"], - InputWorkspace=groceries["inputWorkspace"] + InputWorkspace=groceries["inputWorkspace"], ) mockSnapper.EditInstrumentGeometry.assert_called_once_with( f"Editing instrument geometry for grouping '{ingredients.unmaskedPixelGroup.focusGroup.name}'", @@ -126,7 +119,7 @@ def test_cook(self): L2=np.rad2deg(ingredients.unmaskedPixelGroup.L2), Polar=np.rad2deg(ingredients.unmaskedPixelGroup.twoTheta), Azimuthal=np.rad2deg(ingredients.unmaskedPixelGroup.azimuth), - InstrumentName=f"SNAP_{ingredients.unmaskedPixelGroup.focusGroup.name}" + InstrumentName=f"SNAP_{ingredients.unmaskedPixelGroup.focusGroup.name}", ) def test_cook_default(self): @@ -149,7 +142,7 @@ def test_cook_default(self): L2=np.rad2deg(ingredients.unmaskedPixelGroup.L2), Polar=np.rad2deg(ingredients.unmaskedPixelGroup.twoTheta), Azimuthal=np.rad2deg(ingredients.unmaskedPixelGroup.azimuth), - InstrumentName=f"SNAP_{ingredients.unmaskedPixelGroup.focusGroup.name}" + InstrumentName=f"SNAP_{ingredients.unmaskedPixelGroup.focusGroup.name}", ) def test_cater(self): @@ -158,7 +151,7 @@ def test_cater(self): untensils.mantidSnapper = mockSnapper recipe = EffectiveInstrumentRecipe(utensils=untensils) ingredientss = self.ingredientss - + groceriess = [{"inputWorkspace": mock.Mock()}, {"inputWorkspace": mock.Mock()}] recipe.cater(zip(ingredientss, groceriess)) @@ -170,7 +163,7 @@ def test_cater(self): L2=np.rad2deg(ingredientss[0].unmaskedPixelGroup.L2), Polar=np.rad2deg(ingredientss[0].unmaskedPixelGroup.twoTheta), Azimuthal=np.rad2deg(ingredientss[0].unmaskedPixelGroup.azimuth), - InstrumentName=f"SNAP_{ingredientss[0].unmaskedPixelGroup.focusGroup.name}" + InstrumentName=f"SNAP_{ingredientss[0].unmaskedPixelGroup.focusGroup.name}", ) mockSnapper.EditInstrumentGeometry.assert_any_call( f"Editing instrument geometry for grouping '{ingredientss[1].unmaskedPixelGroup.focusGroup.name}'", @@ -178,6 +171,5 @@ def test_cater(self): L2=np.rad2deg(ingredientss[1].unmaskedPixelGroup.L2), Polar=np.rad2deg(ingredientss[1].unmaskedPixelGroup.twoTheta), Azimuthal=np.rad2deg(ingredientss[1].unmaskedPixelGroup.azimuth), - InstrumentName=f"SNAP_{ingredientss[1].unmaskedPixelGroup.focusGroup.name}" + InstrumentName=f"SNAP_{ingredientss[1].unmaskedPixelGroup.focusGroup.name}", ) - diff --git a/tests/unit/backend/recipe/test_PreprocessReductionRecipe.py b/tests/unit/backend/recipe/test_PreprocessReductionRecipe.py index 6de7e038b..7e344885f 100644 --- a/tests/unit/backend/recipe/test_PreprocessReductionRecipe.py +++ b/tests/unit/backend/recipe/test_PreprocessReductionRecipe.py @@ -1,17 +1,18 @@ +import unittest + from mantid.simpleapi import ( CreateEmptyTableWorkspace, CreateSampleWorkspace, LoadInstrument, mtd, ) +from snapred.backend.dao.ingredients import PreprocessReductionIngredients as Ingredients from snapred.backend.recipe.algorithm.Utensils import Utensils from snapred.backend.recipe.PreprocessReductionRecipe import PreprocessReductionRecipe -from snapred.backend.dao.ingredients import PreprocessReductionIngredients as Ingredients from snapred.meta.Config import Resource from util.helpers import createCompatibleMask from util.SculleryBoy import SculleryBoy -import unittest class PreprocessReductionRecipeTest(unittest.TestCase): fakeInstrumentFilePath = Resource.getPath("inputs/testInstrument/fakeSNAP_Definition.xml") diff --git a/tests/unit/backend/recipe/test_ReductionRecipe.py b/tests/unit/backend/recipe/test_ReductionRecipe.py index a84d48992..2025d1406 100644 --- a/tests/unit/backend/recipe/test_ReductionRecipe.py +++ b/tests/unit/backend/recipe/test_ReductionRecipe.py @@ -1,5 +1,7 @@ import time +from unittest import TestCase, mock +import pytest from mantid.simpleapi import CreateSingleValuedWorkspace, mtd from snapred.backend.dao.ingredients import ReductionIngredients from snapred.backend.recipe.ReductionRecipe import ( @@ -13,8 +15,6 @@ from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceNameGenerator as wng from util.SculleryBoy import SculleryBoy -from unittest import TestCase, mock -import pytest class ReductionRecipeTest(TestCase): sculleryBoy = SculleryBoy() @@ -466,7 +466,7 @@ def test_execute(self, mockMtd): normalizationWorkspace="norm_grouped", ) - # TODO: *** WHY IS THIS SECTION DIFFERENT THAN THE OTHERS?! *** + # TODO: *** WHY IS THIS SECTION DIFFERENT THAN THE OTHERS?! *** artNormCalls = recipe._prepareArtificialNormalization.call_args_list anCall1 = artNormCalls[0] anCall2 = artNormCalls[1] diff --git a/tests/unit/backend/service/test_ReductionService.py b/tests/unit/backend/service/test_ReductionService.py index 271d8dd57..e34e1ee4c 100644 --- a/tests/unit/backend/service/test_ReductionService.py +++ b/tests/unit/backend/service/test_ReductionService.py @@ -79,7 +79,7 @@ def setUp(self): keepUnfocused=True, convertUnitsTo="TOF", focusGroups=[FocusGroup(name="apple", definition="path/to/grouping")], - artificialNormalizationIngredients=mock.Mock(spec=ArtificialNormalizationIngredients) + artificialNormalizationIngredients=mock.Mock(spec=ArtificialNormalizationIngredients), ) def test_name(self): @@ -107,7 +107,7 @@ def test_fetchReductionGroupings(self): def test_prepReductionIngredients(self): # Call the method with the provided parameters result = self.instance.prepReductionIngredients(self.request) - + farmFresh = FarmFreshIngredients( runNumber=self.request.runNumber, useLiteMode=self.request.useLiteMode, @@ -119,7 +119,7 @@ def test_prepReductionIngredients(self): ) expected = self.instance.sousChef.prepReductionIngredients(farmFresh) expected.artificialNormalizationIngredients = self.request.artificialNormalizationIngredients - + assert ReductionIngredients.model_validate(result) assert result == expected @@ -161,11 +161,7 @@ def test_reduction(self, mockReductionRecipe): @mock.patch(thisService + "ReductionRecipe") def test_reduction_full_sequence(self, mockReductionRecipe, mockReductionResponse): mockReductionRecipe.return_value = mock.Mock() - mockResult = { - "result": True, - "outputs": ["one", "two", "three"], - "unfocusedWS": mock.Mock() - } + mockResult = {"result": True, "outputs": ["one", "two", "three"], "unfocusedWS": mock.Mock()} mockReductionRecipe.return_value.cook = mock.Mock(return_value=mockResult) self.instance.dataFactoryService.getThisOrLatestCalibrationVersion = mock.Mock(return_value=1) self.instance.dataFactoryService.stateExists = mock.Mock(return_value=True) @@ -175,44 +171,34 @@ def test_reduction_full_sequence(self, mockReductionRecipe, mockReductionRespons self.instance._markWorkspaceMetadata = mock.Mock() self.instance.fetchReductionGroupings = mock.Mock( - return_value={ - "focusGroups": mock.Mock(), - "groupingWorkspaces": mock.Mock() - } + return_value={"focusGroups": mock.Mock(), "groupingWorkspaces": mock.Mock()} ) - self.instance.fetchReductionGroceries = mock.Mock( - return_value={ - "combinedPixelMask": mock.Mock() - } - ) - self.instance.prepReductionIngredients = mock.Mock( - return_value=mock.Mock() - ) - self.instance._createReductionRecord = mock.Mock( - return_value=mock.Mock() - ) - + self.instance.fetchReductionGroceries = mock.Mock(return_value={"combinedPixelMask": mock.Mock()}) + self.instance.prepReductionIngredients = mock.Mock(return_value=mock.Mock()) + self.instance._createReductionRecord = mock.Mock(return_value=mock.Mock()) + request_ = self.request.model_copy() self.instance.reduction(request_) - + self.instance.fetchReductionGroupings.assert_called_once_with(request_) assert request_.focusGroups == self.instance.fetchReductionGroupings.return_value["focusGroups"] self.instance.fetchReductionGroceries.assert_called_once_with(request_) self.instance.prepReductionIngredients.assert_called_once_with( - request_, - self.instance.fetchReductionGroceries.return_value["combinedPixelMask"] + request_, self.instance.fetchReductionGroceries.return_value["combinedPixelMask"] + ) + assert ( + self.instance.fetchReductionGroceries.return_value["groupingWorkspaces"] + == self.instance.fetchReductionGroupings.return_value["groupingWorkspaces"] ) - assert self.instance.fetchReductionGroceries.return_value["groupingWorkspaces"] ==\ - self.instance.fetchReductionGroupings.return_value["groupingWorkspaces"] - + self.instance._createReductionRecord.assert_called_once_with( request_, self.instance.prepReductionIngredients.return_value, - mockReductionRecipe.return_value.cook.return_value["outputs"] - ) + mockReductionRecipe.return_value.cook.return_value["outputs"], + ) mockReductionResponse.assert_called_once_with( record=self.instance._createReductionRecord.return_value, - unfocusedData=mockReductionRecipe.return_value.cook.return_value["unfocusedWS"] + unfocusedData=mockReductionRecipe.return_value.cook.return_value["unfocusedWS"], ) def test_reduction_noState_withWritePerms(self): diff --git a/tests/unit/backend/service/test_SousChef.py b/tests/unit/backend/service/test_SousChef.py index daca80271..49eddb308 100644 --- a/tests/unit/backend/service/test_SousChef.py +++ b/tests/unit/backend/service/test_SousChef.py @@ -172,10 +172,10 @@ def test_prepPixelGroup_nocache( ): self.instance = SousChef() self.instance.dataFactoryService.calibrationExists = mock.Mock(return_value=True) - + # Warning: key now includes pixel mask name. key = (self.ingredients.runNumber, self.ingredients.useLiteMode, self.ingredients.focusGroup.name, None) - + # ensure there is no cached value assert self.instance._pixelGroupCache == {} @@ -206,7 +206,12 @@ def test_prepPixelGroup_nocache( @mock.patch(thisService + "PixelGroupingParametersCalculationRecipe") def test_prepPixelGroup_cache(self, PixelGroupingParametersCalculationRecipe): - key = (self.ingredients.runNumber, self.ingredients.useLiteMode, self.ingredients.focusGroup.name, self.pixelMask) + key = ( + self.ingredients.runNumber, + self.ingredients.useLiteMode, + self.ingredients.focusGroup.name, + self.pixelMask, + ) # ensure the cache is prepared self.instance._pixelGroupCache[key] = mock.sentinel.pixel @@ -473,18 +478,18 @@ def test_prepReductionIngredients(self, ReductionIngredients, mockOS): # noqa: ingredients_.cifPath = self.instance.dataFactoryService.getCifFilePath.return_value # ... from normalization record: ingredients_.calibrantSamplePath = normalizationCalibrantSamplePath - + combinedMask = mock.Mock() # Note that `prepReductionIngredients` is called with the _unmodified_ ingredients. result = self.instance.prepReductionIngredients(self.ingredients, combinedMask) assert self.instance.prepManyPixelGroups.call_count == 2 - + self.instance.prepManyPixelGroups.assert_any_call(ingredients_) self.instance.prepManyPixelGroups.assert_any_call(ingredients_, combinedMask) - + self.instance.dataFactoryService.getCifFilePath.assert_called_once_with("sample") - + ReductionIngredients.assert_called_once_with( runNumber=ingredients_.runNumber, useLiteMode=ingredients_.useLiteMode, diff --git a/tests/unit/ui/workflow/test_DiffCalWorkflow.py b/tests/unit/ui/workflow/test_DiffCalWorkflow.py index 6db0b3501..7af33de47 100644 --- a/tests/unit/ui/workflow/test_DiffCalWorkflow.py +++ b/tests/unit/ui/workflow/test_DiffCalWorkflow.py @@ -1,7 +1,5 @@ from random import randint - -from qtpy.QtCore import Qt -from qtpy.QtWidgets import QApplication, QMessageBox +from unittest.mock import MagicMock, patch from mantid.simpleapi import ( CreateSingleValuedWorkspace, @@ -9,11 +7,11 @@ GroupWorkspaces, mtd, ) +from qtpy.QtWidgets import QMessageBox from snapred.meta.mantid.FitPeaksOutput import FIT_PEAK_DIAG_SUFFIX, FitOutputEnum from snapred.meta.pointer import create_pointer from snapred.ui.workflow.DiffCalWorkflow import DiffCalWorkflow -from unittest.mock import MagicMock, patch @patch("snapred.ui.workflow.DiffCalWorkflow.WorkflowImplementer.request") def test_purge_bad_peaks(workflowRequest, qtbot): # noqa: ARG001 @@ -140,7 +138,7 @@ def test_purge_bad_peaks_too_few(workflowRequest, qtbot): # noqa: ARG001 # # Using a mock here bypasses the following issues: - # + # # * which thread the messagebox will be running on (may cause a segfault); # # * how long to wait for the messagebox to instantiate. @@ -149,13 +147,13 @@ def _tooFewPeaksQuery(_parent, title, text, _buttons): if title == "Too Few Peaks": return QMessageBox.Ok raise RuntimeError(f"unexpected `QMessageBox.critical`: title: {title}, text: {text}") - - mockTooFewPeaksQuery = patch( "qtpy.QtWidgets.QMessageBox.critical", _tooFewPeaksQuery) - + + mockTooFewPeaksQuery = patch("qtpy.QtWidgets.QMessageBox.critical", _tooFewPeaksQuery) + # Use `start` and `stop` rather than `with patch...` in order to apply the mock even in the case of exceptions. mockTooFewPeaksQuery.start() diffcalWorkflow.purgeBadPeaks(maxChiSq) - + # Remember to remove the mock. mockTooFewPeaksQuery.stop() diff --git a/tests/util/SculleryBoy.py b/tests/util/SculleryBoy.py index ec485379c..9ed27360e 100644 --- a/tests/util/SculleryBoy.py +++ b/tests/util/SculleryBoy.py @@ -15,8 +15,8 @@ from snapred.backend.dao.state.PixelGroup import PixelGroup from snapred.backend.dao.state.PixelGroupingParameters import PixelGroupingParameters from snapred.backend.recipe.GenericRecipe import DetectorPeakPredictorRecipe -from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceName from snapred.meta.Config import Resource +from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceName from snapred.meta.redantic import parse_file_as from util.dao import DAOFactory @@ -92,7 +92,9 @@ def prepDetectorPeaks(self, ingredients: FarmFreshIngredients, purgePeaks=False) except (TypeError, AttributeError): return [mock.Mock(spec_set=GroupPeakList)] - def prepReductionIngredients(self, _ingredients: FarmFreshIngredients, _combinedPixelMask: Optional[WorkspaceName] = None): + def prepReductionIngredients( + self, _ingredients: FarmFreshIngredients, _combinedPixelMask: Optional[WorkspaceName] = None + ): path = Resource.getPath("/inputs/calibration/ReductionIngredients.json") return parse_file_as(ReductionIngredients, path)