From 06301a8804d3ae55cc715e417d3d3cdffbde6f71 Mon Sep 17 00:00:00 2001 From: Kort Travis Date: Thu, 23 Jan 2025 18:08:23 -0500 Subject: [PATCH 1/3] Transfer PV-logs during load of grouping and mask workspaces This is a supporting PR for the reduction live-data work. The changes here allow all loaded workspaces, with the exception of the input data workspace itself, to be reused from reduction cycle to reduction cycle. During development work, it was found that although Mantid's `LoadDiffCal` allows an instrument to be specified, it does not additionally transfer the relevant instrument PV-logs from the donor workspace. The result of this is that any mask or grouping workspace loaded by SNAPRed would not have a defined stateID-SHA. During the reduction process, pixel-mask compatibility is checked using the combination of the stateID-SHA, and the lite-mode state -- ADS-resident pixel-mask workspaces with no stateID-SHA will be skipped as incompatible. Also found during development work, when the detector positions are compared between workspaces written using `SaveNexus` and read back using `LoadNexus`, they fail to match to the Mantid position and rotation target accuracy. For positions, this accuracy is `1.0e-9`m, and for rotations, it is `1.0e-9`m at `1000.0`m, which corresponds to `1.0e-12` radians. The reason for this readback mismatch has to do with the lack of a precision specification when writing the locations to the _legacy_ 'instrument_parameter_map' (in the NeXus file). Unfortunately, this same legacy system is used to initialize the instrument parameters during reload. This write-precision defect has been separately fixed in another Mantid PR. This current PR adds a temporary work around for this issue. This commit includes the following changes: * `LoadCalibrationWorkspaces` and `LoadGroupingDefinition` algorithms, both of which use Mantid's `LoadDiffCal` algorithm, now transfer the relevent instrument PV-logs to their output workspaces; * Any `FetchGroceriesAlgorithm` loader which goes through the `LoadNexus` pathway, now additionally calls `populateInstrumentParameters` for the output workspace. This bypasses the inaccuracy of the legacy 'instrument_parameter_map', and directly utilizes the PV-log values, which are stored in their binary representation as double-precision values; * General utilities `transferInstrumentPVLogs` and `populateInstrumentParameters` are provided in `snapred.backend.data.util.PV_logs_util`. The `populateInstrumentParameters` utility is a temporary work around, as `ExperimentInfo.populateInstrumentParameters` has now been exposed to the Python API in another Mantid PR. --- src/snapred/backend/data/util/PV_logs_util.py | 42 +++++ .../backend/recipe/FetchGroceriesRecipe.py | 2 +- .../algorithm/FetchGroceriesAlgorithm.py | 12 ++ .../algorithm/LoadCalibrationWorkspaces.py | 24 ++- .../algorithm/LoadGroupingDefinition.py | 17 +- src/snapred/resources/application.yml | 17 ++ .../test_workflow_panels_happy_path.py | 26 ++- tests/resources/application.yml | 17 ++ .../unit/backend/data/test_GroceryService.py | 45 +++-- .../backend/data/util/test_PV_logs_util.py | 167 ++++++++++++++++++ tests/util/instrument_helpers.py | 14 +- 11 files changed, 350 insertions(+), 33 deletions(-) create mode 100644 src/snapred/backend/data/util/PV_logs_util.py create mode 100644 tests/unit/backend/data/util/test_PV_logs_util.py diff --git a/src/snapred/backend/data/util/PV_logs_util.py b/src/snapred/backend/data/util/PV_logs_util.py new file mode 100644 index 000000000..576c167cf --- /dev/null +++ b/src/snapred/backend/data/util/PV_logs_util.py @@ -0,0 +1,42 @@ +""" +Python `Mapping`-interface adapters and utility-methods relating to Mantid workspace logs (i.e. `Run`) + and process-variable(PV) logs. +""" +# Note: in the upcoming reduction live-data PR this file includes several additional `Mapping` adapters. + +from collections.abc import Iterable + +from mantid.api import Run +from mantid.simpleapi import ( + AddSampleLog, + mtd +) + +from snapred.meta.Config import Config + +def transferInstrumentPVLogs(dest: Run, src: Run, keys: Iterable): + # Transfer instrument-specific PV-log values, between the `Run` attributes + # of source and destination workspaces. + + # Placed here for use by various `FetchGroceriesAlgorithm` loaders. + for key in keys: + if src.hasProperty(key): + dest.addProperty(key, src.getProperty(key), True) + # REMINDER: the instrument-parameter update still needs to be explicitly triggered! + +def populateInstrumentParameters(wsName: str): + # This utility function is a "stand in" until Mantid PR #38684 can be merged. + # (see https://github.com/mantidproject/mantid/pull/38684) + # After that, `mtd[wsName].populateInstrumentParameters()` should be used instead. + + # Any PV-log key will do, so long as it is one that always exists in the logs. + pvLogKey = "run_title" + pvLogValue = mtd[wsName].run().getProperty(pvLogKey).value + + AddSampleLog( + Workspace=wsName, + LogName=pvLogKey, + logText=pvLogValue, + logType="String", + UpdateInstrumentParameters=True, + ) diff --git a/src/snapred/backend/recipe/FetchGroceriesRecipe.py b/src/snapred/backend/recipe/FetchGroceriesRecipe.py index e8dd2ed7c..9335874d0 100644 --- a/src/snapred/backend/recipe/FetchGroceriesRecipe.py +++ b/src/snapred/backend/recipe/FetchGroceriesRecipe.py @@ -58,7 +58,7 @@ def executeRecipe( data["result"] = algo.execute() data["loader"] = algo.getPropertyValue("LoaderType") data["workspace"] = workspace - + if data["loader"] == "LoadEventNexus": self.dataService = LocalDataService() config = self.dataService.readInstrumentConfig() diff --git a/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py b/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py index 42311d755..1a58331cf 100644 --- a/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py +++ b/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py @@ -14,6 +14,7 @@ StringListValidator, ) +from snapred.backend.data.util.PV_logs_util import populateInstrumentParameters from snapred.backend.log.logger import snapredLogger from snapred.backend.recipe.algorithm.MantidSnapper import MantidSnapper @@ -147,6 +148,17 @@ def PyExec(self) -> None: logger.warning(f"A workspace with name {outWS} already exists in the ADS, and so will not be loaded") loaderType = "" self.mantidSnapper.executeQueue() + + if loaderType in ["LoadNexus", "LoadEventNexus", "LoadNexusProcessed"]: + # TODO: See EWM#7437: + # this clause is necessary to be able to accurately set detector positions + # on files written prior to the merge of the + # `SaveNexus` 'instrument_parameter_map' write-precision fix. + # It probably should not be removed, even after that fix is merged. + # It should be replaced with `mtd[workspace].updateInstrumentParameters()` after + # Mantid PR#38684 has been merged. + populateInstrumentParameters(outWS) + self.setPropertyValue("OutputWorkspace", outWS) self.setPropertyValue("LoaderType", str(loaderType)) diff --git a/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py b/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py index 252826530..530173732 100644 --- a/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py +++ b/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py @@ -14,11 +14,13 @@ from mantid.dataobjects import MaskWorkspaceProperty from mantid.kernel import Direction +from snapred.backend.data.util.PV_logs_util import populateInstrumentParameters, transferInstrumentPVLogs from snapred.backend.log.logger import snapredLogger from snapred.backend.recipe.algorithm.MantidSnapper import MantidSnapper +from snapred.meta.Config import Config -logger = snapredLogger.getLogger(__name__) +logger = snapredLogger.getLogger(__name__) class LoadCalibrationWorkspaces(PythonAlgorithm): """ @@ -30,7 +32,7 @@ class LoadCalibrationWorkspaces(PythonAlgorithm): InstrumentDonor: str -- name of the instrument donor workspace CalibrationTable: str -- name of the output table workspace MaskWorkspace: str -- name of the output mask workspace - GroupingWorkspace: str -- name of the output mask workspace + GroupingWorkspace: str -- name of the output grouping workspace """ def category(self): @@ -65,7 +67,7 @@ def PyInit(self) -> None: ) self.declareProperty( MatrixWorkspaceProperty("GroupingWorkspace", "", Direction.Output, PropertyMode.Optional), - doc="Name of the output mask workspace", + doc="Name of the output grouping workspace", ) self.setRethrows(True) @@ -134,6 +136,22 @@ def PyExec(self) -> None: OutputWorkspace=self.groupingWorkspace, ) self.mantidSnapper.executeQueue() + + # Transfer the instrument PV-logs -- not done yet by `LoadDiffCal`. + if self.maskWorkspace: + transferInstrumentPVLogs( + self.mantidSnapper.mtd[self.maskWorkspace].mutableRun(), + self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), + Config["instrument.PVLogs.instrumentKeys"] + ) + populateInstrumentParameters(self.maskWorkspace) + if self.groupingWorkspace: + transferInstrumentPVLogs( + self.mantidSnapper.mtd[self.groupingWorkspace].mutableRun(), + self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), + Config["instrument.PVLogs.instrumentKeys"] + ) + populateInstrumentParameters(self.groupingWorkspace) if self.calibrationTable: # NOTE ConvertDiffCal has issues ifthe "tofmin" column is present, diff --git a/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py b/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py index 745b14f90..9af164139 100644 --- a/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py +++ b/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py @@ -12,12 +12,13 @@ ) from mantid.kernel import Direction +from snapred.backend.data.util.PV_logs_util import populateInstrumentParameters, transferInstrumentPVLogs from snapred.backend.log.logger import snapredLogger from snapred.backend.recipe.algorithm.MantidSnapper import MantidSnapper from snapred.meta.Config import Config -logger = snapredLogger.getLogger(__name__) +logger = snapredLogger.getLogger(__name__) class LoadGroupingDefinition(PythonAlgorithm): """ @@ -69,7 +70,7 @@ def PyInit(self) -> None: ) self.declareProperty( MatrixWorkspaceProperty("InstrumentDonor", "", Direction.Input, PropertyMode.Optional), - doc="Workspace to optionally take the instrument from, when GroupingFilename is in XML format", + doc="Workspace to optionally take the instrument from", ) self.declareProperty( MatrixWorkspaceProperty("OutputWorkspace", "", Direction.Output, PropertyMode.Mandatory), @@ -145,6 +146,15 @@ def PyExec(self) -> None: InputWorkspace=self.outputWorkspaceName + "_group", OutputWorkspace=self.outputWorkspaceName, ) + self.mantidSnapper.executeQueue() + if not self.getProperty("InstrumentDonor").isDefault: + # Transfer the instrument PV-logs -- not done yet by `LoadDiffCal`. + transferInstrumentPVLogs( + self.mantidSnapper.mtd[self.outputWorkspaceName].mutableRun(), + self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), + Config["instrument.PVLogs.instrumentKeys"] + ) + populateInstrumentParameters(self.outputWorkspaceName) elif self.groupingFileExt in self.supported_xml_file_extensions: instrument_donor = self.getPropertyValue("InstrumentDonor") preserve_donor = True if instrument_donor else False @@ -168,12 +178,14 @@ def PyExec(self) -> None: "Deleting instrument definition workspace...", Workspace=instrument_donor, ) + self.mantidSnapper.executeQueue() elif self.groupingFileExt in self.supported_nexus_file_extensions: # must be a NEXUS file self.mantidSnapper.LoadNexusProcessed( "Loading grouping definition from grouping workspace...", Filename=self.groupingFilePath, OutputWorkspace=self.outputWorkspaceName, ) + self.mantidSnapper.executeQueue() else: raise RuntimeError( f""" @@ -182,7 +194,6 @@ def PyExec(self) -> None: extensions are {self.all_extensions} """ ) - self.mantidSnapper.executeQueue() self.setPropertyValue("OutputWorkspace", self.outputWorkspaceName) diff --git a/src/snapred/resources/application.yml b/src/snapred/resources/application.yml index 9f63239a0..e357655d9 100644 --- a/src/snapred/resources/application.yml +++ b/src/snapred/resources/application.yml @@ -46,6 +46,23 @@ instrument: map: file: ${instrument.calibration.home}/Powder/LiteGroupMap.hdf # file: ${module.root}/resources/ultralite/CRACKLELiteDataMap.xml + + PVLogs: + # Swap these when running with ultralite data + rootGroup: "entry/DASlogs" + # rootGroup: "mantid_workspace_1/logs" + + # PV-log keys relating to instrument settings: + instrumentKeys: + - "BL3:Chop:Gbl:WavelengthReq" + - "BL3:Chop:Skf1:WavelengthUserReq" + - "det_arc1" + - "det_arc2" + - "BL3:Det:TH:BL:Frequency" + - "BL3:Mot:OpticsPos:Pos" + - "det_lin1" + - "det_lin2" + startingRunNumber: 10000 minimumRunNumber: 46342 maxNumberOfRuns: 10 diff --git a/tests/integration/test_workflow_panels_happy_path.py b/tests/integration/test_workflow_panels_happy_path.py index bf31a6111..8f042493b 100644 --- a/tests/integration/test_workflow_panels_happy_path.py +++ b/tests/integration/test_workflow_panels_happy_path.py @@ -1,20 +1,25 @@ +import os import re +import tempfile from contextlib import ExitStack, suppress -from unittest import mock - -import pytest -from mantid.kernel import amend_config +from pathlib import Path from qtpy import QtCore from qtpy.QtCore import Qt from qtpy.QtWidgets import ( QMessageBox, QTabWidget, ) -from util.pytest_helpers import calibration_home_from_mirror, handleStateInit, reduction_home_from_mirror # noqa: F401 -from util.TestSummary import TestSummary +from typing import Optional -from snapred.meta.Config import Resource +from mantid.kernel import amend_config + +# I would prefer not to access `LocalDataService` within an integration test, +# however, for the moment, the reduction-data output relocation fixture is defined in the current file. +from snapred.backend.data.LocalDataService import LocalDataService +from snapred.meta.Config import Config, Resource +from snapred.meta.Enum import StrEnum from snapred.ui.main import SNAPRedGUI, prependDataSearchDirectories +from snapred.ui.view import InitializeStateCheckView from snapred.ui.view.DiffCalAssessmentView import DiffCalAssessmentView from snapred.ui.view.DiffCalRequestView import DiffCalRequestView from snapred.ui.view.DiffCalSaveView import DiffCalSaveView @@ -27,6 +32,11 @@ # TODO: WorkflowNodeComplete signal, at end of each node! +# Add test-related imports at the end, in order to preserve the import sequence as much as possible. +from unittest import mock +import pytest +from util.Config_helpers import Config_override +from util.script_as_test import not_a_test class InterruptWithBlock(BaseException): pass @@ -494,7 +504,7 @@ def test_calibration_and_reduction_panels_happy_path( # Force a clean exit qtbot.wait(5000) - def test_diffraction_calibration_panel_happy_path(self, qtbot, qapp, calibration_home_from_mirror): # noqa: F811 + def test_diffraction_calibration_panel_happy_path(self, qtbot, qapp, calibration_home_from_mirror): # Override the mirror with a new home directory, omitting any existing # calibration or normalization data. tmpCalibrationHomeDirectory = calibration_home_from_mirror() # noqa: F841 diff --git a/tests/resources/application.yml b/tests/resources/application.yml index 9c5ddcb4b..4adf0a3b5 100644 --- a/tests/resources/application.yml +++ b/tests/resources/application.yml @@ -46,6 +46,23 @@ instrument: file: ${module.root}/resources/inputs/pixel_grouping/SNAPLite_Definition.xml map: file: ${instrument.calibration.home}/Powder/LiteGroupMap.hdf + + PVLogs: + # Swap these when running with ultralite data + rootGroup: "entry/DASlogs" + # rootGroup: "mantid_workspace_1/logs" + + # PV-log keys relating to instrument settings: + instrumentKeys: + - "BL3:Chop:Gbl:WavelengthReq" + - "BL3:Chop:Skf1:WavelengthUserReq" + - "det_arc1" + - "det_arc2" + - "BL3:Det:TH:BL:Frequency" + - "BL3:Mot:OpticsPos:Pos" + - "det_lin1" + - "det_lin2" + startingRunNumber: 10000 minimumRunNumber: 46342 maxNumberOfRuns: 10 diff --git a/tests/unit/backend/data/test_GroceryService.py b/tests/unit/backend/data/test_GroceryService.py index c1f357ca0..f075bfd30 100644 --- a/tests/unit/backend/data/test_GroceryService.py +++ b/tests/unit/backend/data/test_GroceryService.py @@ -3,12 +3,9 @@ import os import shutil import time -import unittest from pathlib import Path from random import randint -from unittest import mock -import pytest from mantid.dataobjects import MaskWorkspace from mantid.kernel import V3D, Quat from mantid.simpleapi import ( @@ -27,25 +24,37 @@ SaveNexusProcessed, mtd, ) -from mantid.testing import assert_almost_equal as assert_wksp_almost_equal -from util.Config_helpers import Config_override -from util.helpers import createCompatibleDiffCalTable, createCompatibleMask -from util.instrument_helpers import mapFromSampleLogs -from util.kernel_helpers import tupleFromQuat, tupleFromV3D -from util.state_helpers import reduction_root_redirect, state_root_redirect -from util.WhateversInTheFridge import WhateversInTheFridge import snapred.backend.recipe.algorithm # noqa: F401 from snapred.backend.dao.ingredients.GroceryListItem import GroceryListItem from snapred.backend.dao.state import DetectorState from snapred.backend.dao.WorkspaceMetadata import UNSET, DiffcalStateMetadata, WorkspaceMetadata from snapred.backend.data.GroceryService import GroceryService +from snapred.backend.data.util.PV_logs_util import populateInstrumentParameters from snapred.meta.Config import Config, Resource from snapred.meta.InternalConstants import ReservedRunNumber from snapred.meta.mantid.WorkspaceNameGenerator import ValueFormatter as wnvf from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceNameGenerator as wng from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceType +# In order to preserve the import order as much as possible, add test-related imports at the end. +import unittest +from unittest import mock +import pytest +from mantid.testing import assert_almost_equal as assert_wksp_almost_equal +from util.Config_helpers import Config_override +from util.dao import DAOFactory +from util.helpers import createCompatibleDiffCalTable, createCompatibleMask +from util.instrument_helpers import ( + mapFromSampleLogs, + getInstrumentLogDescriptors, + addInstrumentLogs +) +from util.kernel_helpers import tupleFromQuat, tupleFromV3D +from util.state_helpers import reduction_root_redirect, state_root_redirect +from util.WhateversInTheFridge import WhateversInTheFridge + + ThisService = "snapred.backend.data.GroceryService." @@ -90,6 +99,19 @@ def setUpClass(cls): Filename=cls.instrumentFilePath, RewriteSpectraMap=True, ) + + # Clone a copy of the sample workspace, but without any instrument-parameter state. + cls.sampleWSBareInstrument = mtd.unique_hidden_name() + CloneWorkspace( + OutputWorkspace=cls.sampleWSBareInstrument, + InputWorkspace=cls.sampleWS + ) + # Add a complete instrument-parameter state to the sample workspace. + # This is now required of the instrument-donor workspace for `LoadGroupingDefinition` + # and `LoadCalibrationWorkspaces`. + cls.detectorState = DAOFactory.real_detector_state + addInstrumentLogs(cls.sampleWS, **getInstrumentLogDescriptors(cls.detectorState)) + SaveNexusProcessed( InputWorkspace=cls.sampleWS, Filename=cls.sampleWSFilePath, @@ -841,6 +863,7 @@ def test_fetch_dirty_nexus_native(self): # assert the correct workspaces exist assert not mtd.doesExist(rawWorkspaceName) assert mtd.doesExist(workspaceName) + # test the workspace is correct assert_wksp_almost_equal( Workspace1=self.sampleWS, @@ -1445,7 +1468,7 @@ def test_fetch_grocery_list_with_source(self): def test_updateInstrumentParameters(self): wsName = mtd.unique_hidden_name() CloneWorkspace( - InputWorkspace=self.sampleWS, + InputWorkspace=self.sampleWSBareInstrument, OutputWorkspace=wsName, ) assert mtd.doesExist(wsName) diff --git a/tests/unit/backend/data/util/test_PV_logs_util.py b/tests/unit/backend/data/util/test_PV_logs_util.py new file mode 100644 index 000000000..e5da1e76d --- /dev/null +++ b/tests/unit/backend/data/util/test_PV_logs_util.py @@ -0,0 +1,167 @@ +from collections.abc import Iterable +import datetime + +from mantid.simpleapi import ( + CreateSampleWorkspace, + DeleteWorkspace, + DeleteWorkspaces, + LoadInstrument, + mtd, +) + +from snapred.backend.dao.state import DetectorState +from snapred.backend.data.util.PV_logs_util import * +from snapred.meta.Config import Config, Resource + +# In order to keep the rest of the import sequence unmodified: any test-related imports are added at the end. +import unittest +from unittest import mock +import pytest +from util.dao import DAOFactory +from util.instrument_helpers import ( + getInstrumentLogDescriptors, + addInstrumentLogs +) + +class TestTransferInstrumentPVLogs(unittest.TestCase): + + @classmethod + def createSampleWorkspace(cls): + wsName = mtd.unique_hidden_name() + CreateSampleWorkspace( + OutputWorkspace=wsName, + # WorkspaceType="Histogram", + Function="User Defined", + UserDefinedFunction="name=Gaussian,Height=10,PeakCentre=1.2,Sigma=0.2", + Xmin=0, + Xmax=5, + BinWidth=0.001, + XUnit="dSpacing", + NumBanks=4, # must produce same number of pixels as fake instrument + BankPixelWidth=2, # each bank has 4 pixels, 4 banks, 16 total + ) + LoadInstrument( + Workspace=wsName, + Filename=Resource.getPath("inputs/testInstrument/fakeSNAP_Definition.xml"), + RewriteSpectraMap=True, + ) + return wsName + + def setUp(self): + self.wsWithStandardLogs = self.createSampleWorkspace() + + # Add the standard instrument PV-logs to the workspace's `Run` attribute. + self.detectorState = DAOFactory.real_detector_state + self.instrumentKeys = [ + k for k in Config["instrument.PVLogs.instrumentKeys"] if k != "BL3:Chop:Gbl:WavelengthReq" + ] + logsDescriptors = getInstrumentLogDescriptors(self.detectorState) + addInstrumentLogs(self.wsWithStandardLogs, **logsDescriptors) + self.standardLogs = dict(zip(logsDescriptors['logNames'], logsDescriptors['logValues'])) + + # Add the alterate instrument PV-logs. + self.wsWithAlternateLogs = self.createSampleWorkspace() + self.alternateInstrumentKeys = [ + k for k in Config["instrument.PVLogs.instrumentKeys"] if k != "BL3:Chop:Skf1:WavelengthUserReq" + ] + logsDescriptors["logNames"] = [ + k if k != "BL3:Chop:Skf1:WavelengthUserReq" else "BL3:Chop:Gbl:WavelengthReq"\ + for k in logsDescriptors["logNames"] + ] + addInstrumentLogs(self.wsWithAlternateLogs, **logsDescriptors) + self.alternateLogs = dict(zip(logsDescriptors['logNames'], logsDescriptors['logValues'])) + + + def tearDown(self): + DeleteWorkspaces(WorkspaceList=[self.wsWithStandardLogs, self.wsWithAlternateLogs]) + + def test_Config_keys(self): + # Verify that the standard instrument PV-logs have been attached to the test workspace. + # (This test additionally verifies that the `addInstrumentLogs` interface is using the keys from `Config`.) + run = mtd[self.wsWithStandardLogs].run() + for key in Config["instrument.PVLogs.instrumentKeys"]: + if key == "BL3:Chop:Gbl:WavelengthReq": + continue + assert run.hasProperty(key) + assert f"{run.getProperty(key).value[0]:.16f}" == self.standardLogs[key] + + # Verify the test workspace with the alternate instrument PV-logs. + run = mtd[self.wsWithAlternateLogs].run() + for key in Config["instrument.PVLogs.instrumentKeys"]: + if key == "BL3:Chop:Skf1:WavelengthUserReq": + continue + assert run.hasProperty(key) + assert f"{run.getProperty(key).value[0]:.16f}" == self.alternateLogs[key] + + + def verify_transfer(self, srcWs: str, keys: Iterable, alternateKeys: Iterable): + testWs = self.createSampleWorkspace() + ws = mtd[testWs] + for key in keys: + assert not ws.run().hasProperty(key) + + transferInstrumentPVLogs( + mtd[testWs].mutableRun(), + mtd[srcWs].run(), + keys + ) + populateInstrumentParameters(testWs) + + run = mtd[testWs].run() + srcRun = mtd[srcWs].run() + + # Verify the log transfer. + for key in keys: + assert run.hasProperty(key) + assert run.getProperty(key).value == srcRun.getProperty(key).value + + # Verify that there are no extra "alternate" entries. + for key in alternateKeys: + if key not in keys: + assert not run.hasProperty(key) + + DeleteWorkspace(testWs) + + def test_transfer(self): + self.verify_transfer(self.wsWithStandardLogs, self.instrumentKeys, self.alternateInstrumentKeys) + + def test_alternate_transfer(self): + self.verify_transfer(self.wsWithAlternateLogs, self.alternateInstrumentKeys, self.instrumentKeys) + + def test_instrument_update(self): + # The PV-logs are transferred between the workspace's `Run` attributes. + # Any change in an instrument-related PV-log must also be _applied_ as a transformation + # to the workspace's parameterized instrument. + # This test verifies that, following a logs transfer, such an update works correctly. + + testWs = self.createSampleWorkspace() + + # Verify that [some of the] detector pixels of the standard source workspace have been moved + # from their original locations. Here, we don't care about the specifics of the transformation. + originalPixels = mtd[testWs].detectorInfo() + sourcePixels = mtd[self.wsWithStandardLogs].detectorInfo() + instrumentUpdateApplied = False + for n in range(sourcePixels.size()): + if sourcePixels.position(n) != originalPixels.position(n) or sourcePixels.rotation(n) != originalPixels.rotation(n): + instrumentUpdateApplied = True + break + assert instrumentUpdateApplied + + transferInstrumentPVLogs( + mtd[testWs].mutableRun(), + mtd[self.wsWithStandardLogs].run(), + self.instrumentKeys + ) + populateInstrumentParameters(testWs) + + # Verify that the same instrument transformation has been applied to the source and to the destination workspace. + newPixels = mtd[testWs].detectorInfo() + sourcePixels = mtd[self.wsWithStandardLogs].detectorInfo() + instrumentUpdateApplied = True + for n in range(sourcePixels.size()): + # If these don't match _exactly_, then the values have not been transferred at full precision. + if newPixels.position(n) != sourcePixels.position(n) or newPixels.rotation(n) != sourcePixels.rotation(n): + instrumentUpdateApplied = False + break + assert instrumentUpdateApplied + diff --git a/tests/util/instrument_helpers.py b/tests/util/instrument_helpers.py index 6fbf6a11f..ae30d05b6 100644 --- a/tests/util/instrument_helpers.py +++ b/tests/util/instrument_helpers.py @@ -55,13 +55,13 @@ def getInstrumentLogDescriptors(detectorState: DetectorState): "Number Series", ], "logValues": [ - str(detectorState.arc[0]), - str(detectorState.arc[1]), - str(detectorState.wav), - str(detectorState.freq), - str(detectorState.guideStat), - str(detectorState.lin[0]), - str(detectorState.lin[1]), + f"{detectorState.arc[0]:.16f}", + f"{detectorState.arc[1]:.16f}", + f"{detectorState.wav:.16f}", + f"{detectorState.freq:.16f}", + f"{detectorState.guideStat:.16f}", + f"{detectorState.lin[0]:.16f}", + f"{detectorState.lin[1]:.16f}", ], } From 4e92b71bc4ee725cfa315c5c08b4f3911da382c4 Mon Sep 17 00:00:00 2001 From: Kort Travis Date: Thu, 30 Jan 2025 11:01:07 -0500 Subject: [PATCH 2/3] Fixups to rebase --- tests/integration/test_workflow_panels_happy_path.py | 2 ++ tests/unit/backend/data/util/test_PV_logs_util.py | 9 ++++++--- tests/util/TestSummary.py | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_workflow_panels_happy_path.py b/tests/integration/test_workflow_panels_happy_path.py index 8f042493b..879002094 100644 --- a/tests/integration/test_workflow_panels_happy_path.py +++ b/tests/integration/test_workflow_panels_happy_path.py @@ -36,7 +36,9 @@ from unittest import mock import pytest from util.Config_helpers import Config_override +from util.pytest_helpers import handleStateInit from util.script_as_test import not_a_test +from util.TestSummary import TestSummary class InterruptWithBlock(BaseException): pass diff --git a/tests/unit/backend/data/util/test_PV_logs_util.py b/tests/unit/backend/data/util/test_PV_logs_util.py index e5da1e76d..34591adad 100644 --- a/tests/unit/backend/data/util/test_PV_logs_util.py +++ b/tests/unit/backend/data/util/test_PV_logs_util.py @@ -142,7 +142,8 @@ def test_instrument_update(self): sourcePixels = mtd[self.wsWithStandardLogs].detectorInfo() instrumentUpdateApplied = False for n in range(sourcePixels.size()): - if sourcePixels.position(n) != originalPixels.position(n) or sourcePixels.rotation(n) != originalPixels.rotation(n): + if sourcePixels.position(n) != originalPixels.position(n)\ + or sourcePixels.rotation(n) != originalPixels.rotation(n): instrumentUpdateApplied = True break assert instrumentUpdateApplied @@ -154,13 +155,15 @@ def test_instrument_update(self): ) populateInstrumentParameters(testWs) - # Verify that the same instrument transformation has been applied to the source and to the destination workspace. + # Verify that the same instrument transformation + # has been applied to the source and to the destination workspace. newPixels = mtd[testWs].detectorInfo() sourcePixels = mtd[self.wsWithStandardLogs].detectorInfo() instrumentUpdateApplied = True for n in range(sourcePixels.size()): # If these don't match _exactly_, then the values have not been transferred at full precision. - if newPixels.position(n) != sourcePixels.position(n) or newPixels.rotation(n) != sourcePixels.rotation(n): + if newPixels.position(n) != sourcePixels.position(n)\ + or newPixels.rotation(n) != sourcePixels.rotation(n): instrumentUpdateApplied = False break assert instrumentUpdateApplied diff --git a/tests/util/TestSummary.py b/tests/util/TestSummary.py index aa551c421..401a66370 100644 --- a/tests/util/TestSummary.py +++ b/tests/util/TestSummary.py @@ -1,6 +1,7 @@ from snapred.meta.Enum import StrEnum +from util.script_as_test import not_a_test - +@not_a_test class TestSummary: def __init__(self): self._index = 0 From fbb6140746ad8ecf8576903bfa9e9b7d1a93a3fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:01:37 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/snapred/backend/data/util/PV_logs_util.py | 13 ++- .../backend/recipe/FetchGroceriesRecipe.py | 2 +- .../algorithm/FetchGroceriesAlgorithm.py | 6 +- .../algorithm/LoadCalibrationWorkspaces.py | 8 +- .../algorithm/LoadGroupingDefinition.py | 4 +- src/snapred/resources/application.yml | 8 +- .../test_workflow_panels_happy_path.py | 29 +++---- tests/resources/application.yml | 8 +- .../unit/backend/data/test_GroceryService.py | 41 ++++------ .../backend/data/util/test_PV_logs_util.py | 81 +++++++------------ tests/util/TestSummary.py | 4 +- 11 files changed, 83 insertions(+), 121 deletions(-) diff --git a/src/snapred/backend/data/util/PV_logs_util.py b/src/snapred/backend/data/util/PV_logs_util.py index 576c167cf..416ae3e39 100644 --- a/src/snapred/backend/data/util/PV_logs_util.py +++ b/src/snapred/backend/data/util/PV_logs_util.py @@ -7,32 +7,29 @@ from collections.abc import Iterable from mantid.api import Run -from mantid.simpleapi import ( - AddSampleLog, - mtd -) +from mantid.simpleapi import AddSampleLog, mtd -from snapred.meta.Config import Config def transferInstrumentPVLogs(dest: Run, src: Run, keys: Iterable): # Transfer instrument-specific PV-log values, between the `Run` attributes # of source and destination workspaces. - + # Placed here for use by various `FetchGroceriesAlgorithm` loaders. for key in keys: if src.hasProperty(key): dest.addProperty(key, src.getProperty(key), True) # REMINDER: the instrument-parameter update still needs to be explicitly triggered! + def populateInstrumentParameters(wsName: str): # This utility function is a "stand in" until Mantid PR #38684 can be merged. # (see https://github.com/mantidproject/mantid/pull/38684) # After that, `mtd[wsName].populateInstrumentParameters()` should be used instead. - + # Any PV-log key will do, so long as it is one that always exists in the logs. pvLogKey = "run_title" pvLogValue = mtd[wsName].run().getProperty(pvLogKey).value - + AddSampleLog( Workspace=wsName, LogName=pvLogKey, diff --git a/src/snapred/backend/recipe/FetchGroceriesRecipe.py b/src/snapred/backend/recipe/FetchGroceriesRecipe.py index 9335874d0..e8dd2ed7c 100644 --- a/src/snapred/backend/recipe/FetchGroceriesRecipe.py +++ b/src/snapred/backend/recipe/FetchGroceriesRecipe.py @@ -58,7 +58,7 @@ def executeRecipe( data["result"] = algo.execute() data["loader"] = algo.getPropertyValue("LoaderType") data["workspace"] = workspace - + if data["loader"] == "LoadEventNexus": self.dataService = LocalDataService() config = self.dataService.readInstrumentConfig() diff --git a/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py b/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py index 1a58331cf..0aa5d06ff 100644 --- a/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py +++ b/src/snapred/backend/recipe/algorithm/FetchGroceriesAlgorithm.py @@ -148,17 +148,17 @@ def PyExec(self) -> None: logger.warning(f"A workspace with name {outWS} already exists in the ADS, and so will not be loaded") loaderType = "" self.mantidSnapper.executeQueue() - + if loaderType in ["LoadNexus", "LoadEventNexus", "LoadNexusProcessed"]: # TODO: See EWM#7437: # this clause is necessary to be able to accurately set detector positions - # on files written prior to the merge of the + # on files written prior to the merge of the # `SaveNexus` 'instrument_parameter_map' write-precision fix. # It probably should not be removed, even after that fix is merged. # It should be replaced with `mtd[workspace].updateInstrumentParameters()` after # Mantid PR#38684 has been merged. populateInstrumentParameters(outWS) - + self.setPropertyValue("OutputWorkspace", outWS) self.setPropertyValue("LoaderType", str(loaderType)) diff --git a/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py b/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py index 530173732..872b44d1d 100644 --- a/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py +++ b/src/snapred/backend/recipe/algorithm/LoadCalibrationWorkspaces.py @@ -19,9 +19,9 @@ from snapred.backend.recipe.algorithm.MantidSnapper import MantidSnapper from snapred.meta.Config import Config - logger = snapredLogger.getLogger(__name__) + class LoadCalibrationWorkspaces(PythonAlgorithm): """ This algorithm loads any or all of the table workspace, mask workspace, and grouping workspace @@ -136,20 +136,20 @@ def PyExec(self) -> None: OutputWorkspace=self.groupingWorkspace, ) self.mantidSnapper.executeQueue() - + # Transfer the instrument PV-logs -- not done yet by `LoadDiffCal`. if self.maskWorkspace: transferInstrumentPVLogs( self.mantidSnapper.mtd[self.maskWorkspace].mutableRun(), self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), - Config["instrument.PVLogs.instrumentKeys"] + Config["instrument.PVLogs.instrumentKeys"], ) populateInstrumentParameters(self.maskWorkspace) if self.groupingWorkspace: transferInstrumentPVLogs( self.mantidSnapper.mtd[self.groupingWorkspace].mutableRun(), self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), - Config["instrument.PVLogs.instrumentKeys"] + Config["instrument.PVLogs.instrumentKeys"], ) populateInstrumentParameters(self.groupingWorkspace) diff --git a/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py b/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py index 9af164139..0283f5ccd 100644 --- a/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py +++ b/src/snapred/backend/recipe/algorithm/LoadGroupingDefinition.py @@ -17,9 +17,9 @@ from snapred.backend.recipe.algorithm.MantidSnapper import MantidSnapper from snapred.meta.Config import Config - logger = snapredLogger.getLogger(__name__) + class LoadGroupingDefinition(PythonAlgorithm): """ This algorithm creates a grouping workspace from a grouping definition file. @@ -152,7 +152,7 @@ def PyExec(self) -> None: transferInstrumentPVLogs( self.mantidSnapper.mtd[self.outputWorkspaceName].mutableRun(), self.mantidSnapper.mtd[self.getPropertyValue("InstrumentDonor")].run(), - Config["instrument.PVLogs.instrumentKeys"] + Config["instrument.PVLogs.instrumentKeys"], ) populateInstrumentParameters(self.outputWorkspaceName) elif self.groupingFileExt in self.supported_xml_file_extensions: diff --git a/src/snapred/resources/application.yml b/src/snapred/resources/application.yml index e357655d9..8a4915d3a 100644 --- a/src/snapred/resources/application.yml +++ b/src/snapred/resources/application.yml @@ -47,13 +47,13 @@ instrument: file: ${instrument.calibration.home}/Powder/LiteGroupMap.hdf # file: ${module.root}/resources/ultralite/CRACKLELiteDataMap.xml - PVLogs: + PVLogs: # Swap these when running with ultralite data rootGroup: "entry/DASlogs" # rootGroup: "mantid_workspace_1/logs" - + # PV-log keys relating to instrument settings: - instrumentKeys: + instrumentKeys: - "BL3:Chop:Gbl:WavelengthReq" - "BL3:Chop:Skf1:WavelengthUserReq" - "det_arc1" @@ -62,7 +62,7 @@ instrument: - "BL3:Mot:OpticsPos:Pos" - "det_lin1" - "det_lin2" - + startingRunNumber: 10000 minimumRunNumber: 46342 maxNumberOfRuns: 10 diff --git a/tests/integration/test_workflow_panels_happy_path.py b/tests/integration/test_workflow_panels_happy_path.py index 879002094..10df684d2 100644 --- a/tests/integration/test_workflow_panels_happy_path.py +++ b/tests/integration/test_workflow_panels_happy_path.py @@ -1,25 +1,25 @@ -import os import re -import tempfile from contextlib import ExitStack, suppress -from pathlib import Path + +# TODO: WorkflowNodeComplete signal, at end of each node! +# Add test-related imports at the end, in order to preserve the import sequence as much as possible. +from unittest import mock + +import pytest +from mantid.kernel import amend_config from qtpy import QtCore from qtpy.QtCore import Qt from qtpy.QtWidgets import ( QMessageBox, QTabWidget, ) -from typing import Optional - -from mantid.kernel import amend_config +from util.pytest_helpers import handleStateInit +from util.TestSummary import TestSummary # I would prefer not to access `LocalDataService` within an integration test, # however, for the moment, the reduction-data output relocation fixture is defined in the current file. -from snapred.backend.data.LocalDataService import LocalDataService -from snapred.meta.Config import Config, Resource -from snapred.meta.Enum import StrEnum +from snapred.meta.Config import Resource from snapred.ui.main import SNAPRedGUI, prependDataSearchDirectories -from snapred.ui.view import InitializeStateCheckView from snapred.ui.view.DiffCalAssessmentView import DiffCalAssessmentView from snapred.ui.view.DiffCalRequestView import DiffCalRequestView from snapred.ui.view.DiffCalSaveView import DiffCalSaveView @@ -30,15 +30,6 @@ from snapred.ui.view.reduction.ReductionRequestView import ReductionRequestView from snapred.ui.view.reduction.ReductionSaveView import ReductionSaveView -# TODO: WorkflowNodeComplete signal, at end of each node! - -# Add test-related imports at the end, in order to preserve the import sequence as much as possible. -from unittest import mock -import pytest -from util.Config_helpers import Config_override -from util.pytest_helpers import handleStateInit -from util.script_as_test import not_a_test -from util.TestSummary import TestSummary class InterruptWithBlock(BaseException): pass diff --git a/tests/resources/application.yml b/tests/resources/application.yml index 4adf0a3b5..1db6ba21e 100644 --- a/tests/resources/application.yml +++ b/tests/resources/application.yml @@ -47,13 +47,13 @@ instrument: map: file: ${instrument.calibration.home}/Powder/LiteGroupMap.hdf - PVLogs: + PVLogs: # Swap these when running with ultralite data rootGroup: "entry/DASlogs" # rootGroup: "mantid_workspace_1/logs" - + # PV-log keys relating to instrument settings: - instrumentKeys: + instrumentKeys: - "BL3:Chop:Gbl:WavelengthReq" - "BL3:Chop:Skf1:WavelengthUserReq" - "det_arc1" @@ -62,7 +62,7 @@ instrument: - "BL3:Mot:OpticsPos:Pos" - "det_lin1" - "det_lin2" - + startingRunNumber: 10000 minimumRunNumber: 46342 maxNumberOfRuns: 10 diff --git a/tests/unit/backend/data/test_GroceryService.py b/tests/unit/backend/data/test_GroceryService.py index f075bfd30..f9155d219 100644 --- a/tests/unit/backend/data/test_GroceryService.py +++ b/tests/unit/backend/data/test_GroceryService.py @@ -3,9 +3,14 @@ import os import shutil import time + +# In order to preserve the import order as much as possible, add test-related imports at the end. +import unittest from pathlib import Path from random import randint +from unittest import mock +import pytest from mantid.dataobjects import MaskWorkspace from mantid.kernel import V3D, Quat from mantid.simpleapi import ( @@ -24,37 +29,26 @@ SaveNexusProcessed, mtd, ) +from mantid.testing import assert_almost_equal as assert_wksp_almost_equal +from util.Config_helpers import Config_override +from util.dao import DAOFactory +from util.helpers import createCompatibleDiffCalTable, createCompatibleMask +from util.instrument_helpers import addInstrumentLogs, getInstrumentLogDescriptors, mapFromSampleLogs +from util.kernel_helpers import tupleFromQuat, tupleFromV3D +from util.state_helpers import reduction_root_redirect, state_root_redirect +from util.WhateversInTheFridge import WhateversInTheFridge import snapred.backend.recipe.algorithm # noqa: F401 from snapred.backend.dao.ingredients.GroceryListItem import GroceryListItem from snapred.backend.dao.state import DetectorState from snapred.backend.dao.WorkspaceMetadata import UNSET, DiffcalStateMetadata, WorkspaceMetadata from snapred.backend.data.GroceryService import GroceryService -from snapred.backend.data.util.PV_logs_util import populateInstrumentParameters from snapred.meta.Config import Config, Resource from snapred.meta.InternalConstants import ReservedRunNumber from snapred.meta.mantid.WorkspaceNameGenerator import ValueFormatter as wnvf from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceNameGenerator as wng from snapred.meta.mantid.WorkspaceNameGenerator import WorkspaceType -# In order to preserve the import order as much as possible, add test-related imports at the end. -import unittest -from unittest import mock -import pytest -from mantid.testing import assert_almost_equal as assert_wksp_almost_equal -from util.Config_helpers import Config_override -from util.dao import DAOFactory -from util.helpers import createCompatibleDiffCalTable, createCompatibleMask -from util.instrument_helpers import ( - mapFromSampleLogs, - getInstrumentLogDescriptors, - addInstrumentLogs -) -from util.kernel_helpers import tupleFromQuat, tupleFromV3D -from util.state_helpers import reduction_root_redirect, state_root_redirect -from util.WhateversInTheFridge import WhateversInTheFridge - - ThisService = "snapred.backend.data.GroceryService." @@ -102,16 +96,13 @@ def setUpClass(cls): # Clone a copy of the sample workspace, but without any instrument-parameter state. cls.sampleWSBareInstrument = mtd.unique_hidden_name() - CloneWorkspace( - OutputWorkspace=cls.sampleWSBareInstrument, - InputWorkspace=cls.sampleWS - ) + CloneWorkspace(OutputWorkspace=cls.sampleWSBareInstrument, InputWorkspace=cls.sampleWS) # Add a complete instrument-parameter state to the sample workspace. # This is now required of the instrument-donor workspace for `LoadGroupingDefinition` # and `LoadCalibrationWorkspaces`. cls.detectorState = DAOFactory.real_detector_state addInstrumentLogs(cls.sampleWS, **getInstrumentLogDescriptors(cls.detectorState)) - + SaveNexusProcessed( InputWorkspace=cls.sampleWS, Filename=cls.sampleWSFilePath, @@ -863,7 +854,7 @@ def test_fetch_dirty_nexus_native(self): # assert the correct workspaces exist assert not mtd.doesExist(rawWorkspaceName) assert mtd.doesExist(workspaceName) - + # test the workspace is correct assert_wksp_almost_equal( Workspace1=self.sampleWS, diff --git a/tests/unit/backend/data/util/test_PV_logs_util.py b/tests/unit/backend/data/util/test_PV_logs_util.py index 34591adad..703ea2288 100644 --- a/tests/unit/backend/data/util/test_PV_logs_util.py +++ b/tests/unit/backend/data/util/test_PV_logs_util.py @@ -1,5 +1,6 @@ +# In order to keep the rest of the import sequence unmodified: any test-related imports are added at the end. +import unittest from collections.abc import Iterable -import datetime from mantid.simpleapi import ( CreateSampleWorkspace, @@ -8,23 +9,14 @@ LoadInstrument, mtd, ) +from util.dao import DAOFactory +from util.instrument_helpers import addInstrumentLogs, getInstrumentLogDescriptors -from snapred.backend.dao.state import DetectorState from snapred.backend.data.util.PV_logs_util import * from snapred.meta.Config import Config, Resource -# In order to keep the rest of the import sequence unmodified: any test-related imports are added at the end. -import unittest -from unittest import mock -import pytest -from util.dao import DAOFactory -from util.instrument_helpers import ( - getInstrumentLogDescriptors, - addInstrumentLogs -) class TestTransferInstrumentPVLogs(unittest.TestCase): - @classmethod def createSampleWorkspace(cls): wsName = mtd.unique_hidden_name() @@ -46,10 +38,10 @@ def createSampleWorkspace(cls): RewriteSpectraMap=True, ) return wsName - + def setUp(self): self.wsWithStandardLogs = self.createSampleWorkspace() - + # Add the standard instrument PV-logs to the workspace's `Run` attribute. self.detectorState = DAOFactory.real_detector_state self.instrumentKeys = [ @@ -57,21 +49,20 @@ def setUp(self): ] logsDescriptors = getInstrumentLogDescriptors(self.detectorState) addInstrumentLogs(self.wsWithStandardLogs, **logsDescriptors) - self.standardLogs = dict(zip(logsDescriptors['logNames'], logsDescriptors['logValues'])) + self.standardLogs = dict(zip(logsDescriptors["logNames"], logsDescriptors["logValues"])) # Add the alterate instrument PV-logs. self.wsWithAlternateLogs = self.createSampleWorkspace() self.alternateInstrumentKeys = [ k for k in Config["instrument.PVLogs.instrumentKeys"] if k != "BL3:Chop:Skf1:WavelengthUserReq" - ] + ] logsDescriptors["logNames"] = [ - k if k != "BL3:Chop:Skf1:WavelengthUserReq" else "BL3:Chop:Gbl:WavelengthReq"\ - for k in logsDescriptors["logNames"] + k if k != "BL3:Chop:Skf1:WavelengthUserReq" else "BL3:Chop:Gbl:WavelengthReq" + for k in logsDescriptors["logNames"] ] addInstrumentLogs(self.wsWithAlternateLogs, **logsDescriptors) - self.alternateLogs = dict(zip(logsDescriptors['logNames'], logsDescriptors['logValues'])) - - + self.alternateLogs = dict(zip(logsDescriptors["logNames"], logsDescriptors["logValues"])) + def tearDown(self): DeleteWorkspaces(WorkspaceList=[self.wsWithStandardLogs, self.wsWithAlternateLogs]) @@ -84,7 +75,7 @@ def test_Config_keys(self): continue assert run.hasProperty(key) assert f"{run.getProperty(key).value[0]:.16f}" == self.standardLogs[key] - + # Verify the test workspace with the alternate instrument PV-logs. run = mtd[self.wsWithAlternateLogs].run() for key in Config["instrument.PVLogs.instrumentKeys"]: @@ -92,39 +83,34 @@ def test_Config_keys(self): continue assert run.hasProperty(key) assert f"{run.getProperty(key).value[0]:.16f}" == self.alternateLogs[key] - - + def verify_transfer(self, srcWs: str, keys: Iterable, alternateKeys: Iterable): testWs = self.createSampleWorkspace() ws = mtd[testWs] for key in keys: assert not ws.run().hasProperty(key) - - transferInstrumentPVLogs( - mtd[testWs].mutableRun(), - mtd[srcWs].run(), - keys - ) + + transferInstrumentPVLogs(mtd[testWs].mutableRun(), mtd[srcWs].run(), keys) populateInstrumentParameters(testWs) - + run = mtd[testWs].run() srcRun = mtd[srcWs].run() - + # Verify the log transfer. for key in keys: assert run.hasProperty(key) assert run.getProperty(key).value == srcRun.getProperty(key).value - + # Verify that there are no extra "alternate" entries. for key in alternateKeys: if key not in keys: assert not run.hasProperty(key) - + DeleteWorkspace(testWs) - + def test_transfer(self): self.verify_transfer(self.wsWithStandardLogs, self.instrumentKeys, self.alternateInstrumentKeys) - + def test_alternate_transfer(self): self.verify_transfer(self.wsWithAlternateLogs, self.alternateInstrumentKeys, self.instrumentKeys) @@ -133,28 +119,25 @@ def test_instrument_update(self): # Any change in an instrument-related PV-log must also be _applied_ as a transformation # to the workspace's parameterized instrument. # This test verifies that, following a logs transfer, such an update works correctly. - + testWs = self.createSampleWorkspace() - + # Verify that [some of the] detector pixels of the standard source workspace have been moved # from their original locations. Here, we don't care about the specifics of the transformation. originalPixels = mtd[testWs].detectorInfo() sourcePixels = mtd[self.wsWithStandardLogs].detectorInfo() instrumentUpdateApplied = False for n in range(sourcePixels.size()): - if sourcePixels.position(n) != originalPixels.position(n)\ - or sourcePixels.rotation(n) != originalPixels.rotation(n): + if sourcePixels.position(n) != originalPixels.position(n) or sourcePixels.rotation( + n + ) != originalPixels.rotation(n): instrumentUpdateApplied = True break assert instrumentUpdateApplied - - transferInstrumentPVLogs( - mtd[testWs].mutableRun(), - mtd[self.wsWithStandardLogs].run(), - self.instrumentKeys - ) + + transferInstrumentPVLogs(mtd[testWs].mutableRun(), mtd[self.wsWithStandardLogs].run(), self.instrumentKeys) populateInstrumentParameters(testWs) - + # Verify that the same instrument transformation # has been applied to the source and to the destination workspace. newPixels = mtd[testWs].detectorInfo() @@ -162,9 +145,7 @@ def test_instrument_update(self): instrumentUpdateApplied = True for n in range(sourcePixels.size()): # If these don't match _exactly_, then the values have not been transferred at full precision. - if newPixels.position(n) != sourcePixels.position(n)\ - or newPixels.rotation(n) != sourcePixels.rotation(n): + if newPixels.position(n) != sourcePixels.position(n) or newPixels.rotation(n) != sourcePixels.rotation(n): instrumentUpdateApplied = False break assert instrumentUpdateApplied - diff --git a/tests/util/TestSummary.py b/tests/util/TestSummary.py index 401a66370..62b0ffe4b 100644 --- a/tests/util/TestSummary.py +++ b/tests/util/TestSummary.py @@ -1,6 +1,8 @@ -from snapred.meta.Enum import StrEnum from util.script_as_test import not_a_test +from snapred.meta.Enum import StrEnum + + @not_a_test class TestSummary: def __init__(self):