Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run Feedback Views #554

Open
wants to merge 5 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/snapred/backend/dao/request/RunFeedbackRequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class RunFeedbackRequest(BaseModel):
runId: str
13 changes: 11 additions & 2 deletions src/snapred/backend/dao/state/DetectorState.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import IntEnum
from numbers import Number
from typing import Dict, Literal, Tuple
from typing import Dict, Literal, Optional, Tuple

from pydantic import BaseModel, field_validator

Expand All @@ -17,6 +17,7 @@ class DetectorState(BaseModel):
guideStat: Literal[1, 2]
# two additional values that don't define state, but are useful
lin: Tuple[float, float]
title: Optional[str] = None

@field_validator("guideStat", mode="before")
@classmethod
Expand All @@ -31,13 +32,21 @@ def validate_int(cls, v):
def fromLogs(cls, logs: Dict[str, Number]):
# NeXus/HDF5 and `mantid.api.Run` logs are time-series =>
# here we take only the first entry in each series.
return DetectorState(
ds = DetectorState(
arc=(logs["det_arc1"][0], logs["det_arc2"][0]),
lin=(logs["det_lin1"][0], logs["det_lin2"][0]),
wav=logs.get("BL3:Chop:Skf1:WavelengthUserReq", logs.get("BL3:Chop:Gbl:WavelengthReq"))[0],
freq=logs["BL3:Det:TH:BL:Frequency"][0],
guideStat=int(logs["BL3:Mot:OpticsPos:Pos"][0]),
)
if "title" in logs:
raw = logs["title"]
val = raw[0] if hasattr(raw, "__getitem__") else raw
if hasattr(val, "decode"):
val = val.decode("utf-8")
ds.title = str(val)

return ds

def toLogs(self) -> Dict[str, Number]:
# NeXus/HDF5 and `mantid.api.Run` logs are time-series =>
Expand Down
34 changes: 22 additions & 12 deletions src/snapred/backend/data/util/PV_logs_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,33 @@ def mappingFromNeXusLogs(h5: h5py.File) -> Mapping:

class _Mapping(Mapping):
def __init__(self, h5: h5py.File):
self._logs = h5[Config["instrument.PVLogs.rootGroup"]]
self._logGroup = h5[Config["instrument.PVLogs.rootGroup"]]
self._h5 = h5

def __getitem__(self, key: str) -> Any:
return self._logs[key + "/value"]
if key == "title":
return self._h5["/entry/title"][...]
else:
return self._logGroup[f"{key}/value"][...]

def __iter__(self):
return self.keys().__iter__()
def __contains__(self, key: str) -> bool:
if key == "title":
return "/entry/title" in self._h5
return f"{key}/value" in self._logGroup

def __len__(
self,
):
return len(self._logs.keys())
def keys(self):
baseKeys = []
for fullKey in self._logGroup.keys():
if fullKey.endswith("/value"):
baseKeys.append(fullKey[: fullKey.rfind("/value")])
if "/entry/title" in self._h5:
baseKeys.append("title")
return baseKeys

def __contains__(self, key: str):
return self._logs.__contains__(key + "/value")
def __iter__(self):
return iter(self.keys())

def keys(self):
return [k[0 : k.rfind("/value")] for k in self._logs.keys()]
def __len__(self):
return len(self.keys())

return _Mapping(h5)
7 changes: 7 additions & 0 deletions src/snapred/backend/service/CalibrationService.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
LoadCalibrationRecordRequest,
MatchRunsRequest,
OverrideRequest,
RunFeedbackRequest,
SimpleDiffCalRequest,
)
from snapred.backend.dao.response.CalibrationAssessmentResponse import CalibrationAssessmentResponse
Expand Down Expand Up @@ -105,6 +106,7 @@
self.registerPath("residual", self.calculateResidual)
self.registerPath("fetchMatches", self.fetchMatchingCalibrations)
self.registerPath("override", self.handleOverrides)
self.registerPath("runFeedback", self.runFeedback)
return

@staticmethod
Expand Down Expand Up @@ -603,3 +605,8 @@
return sample.overrides
else:
return None

@FromString
def runFeedback(self, request: RunFeedbackRequest):
stateId = self.dataFactoryService.constructStateId(request.runId)
return stateId

Check warning on line 612 in src/snapred/backend/service/CalibrationService.py

View check run for this annotation

Codecov / codecov/patch

src/snapred/backend/service/CalibrationService.py#L611-L612

Added lines #L611 - L612 were not covered by tests
47 changes: 39 additions & 8 deletions src/snapred/ui/view/DiffCalRequestView.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from qtpy.QtCore import Signal, Slot

from snapred.meta.decorators.Resettable import Resettable
from snapred.meta.mantid.AllowedPeakTypes import SymmetricPeakEnum
from snapred.ui.view.BackendRequestView import BackendRequestView
Expand All @@ -18,19 +20,27 @@ class DiffCalRequestView(BackendRequestView):

"""

signalUpdateRunFeedback = Signal(str, str)

def __init__(self, samples=[], groups=[], parent=None):
super().__init__(parent=parent)

self.signalUpdateRunFeedback.connect(self._setRunFeedback)

# input fields
self.runNumberField = self._labeledField("Run Number")
self.runNumberField.setToolTip("Run number to be calibrated.")
self.liteModeToggle = self._labeledToggle("Lite Mode", True)
self.fieldConvergenceThreshold = self._labeledField("Convergence Threshold")
self.fieldNBinsAcrossPeakWidth = self._labeledField("Bins Across Peak Width")

# drop downs
self.sampleDropdown = self._sampleDropDown("Sample", samples)
self.sampleDropdown.setToolTip("Samples available for this run number.")
self.groupingFileDropdown = self._sampleDropDown("Grouping File", groups)
self.groupingFileDropdown.setToolTip("Grouping schemas available for this sample run number.")
self.peakFunctionDropdown = self._sampleDropDown("Peak Function", [p.value for p in SymmetricPeakEnum])
self.peakFunctionDropdown.setToolTip("Peak function to be used for calibration.")

# checkbox for removing background
self.removeBackgroundToggle = self._labeledToggle("RemoveBackground", False)
Expand All @@ -43,23 +53,36 @@ def __init__(self, samples=[], groups=[], parent=None):
# skip pixel calibration toggle
self.skipPixelCalToggle = self._labeledToggle("Skip Pixel Calibration", False)

# run number feedback fields
self.runFeedbackStateId = self._labeledField("State ID")
self.runFeedbackStateId.setToolTip("State ID of the run number.")
self.runFeedbackRunTitle = self._labeledField("Run Title")
self.runFeedbackRunTitle.setToolTip("Title of the run from PV file.")

# run feedback fields are read only
self.runFeedbackStateId.field.setReadOnly(True)
self.runFeedbackRunTitle.field.setReadOnly(True)

# add all widgets to layout
layout_ = self.layout()
layout_.addWidget(self.runNumberField, 0, 0)
layout_.addWidget(self.liteModeToggle, 0, 1)
layout_.addWidget(self.skipPixelCalToggle, 0, 2)
layout_.addWidget(self.fieldConvergenceThreshold, 1, 0)
layout_.addWidget(self.fieldNBinsAcrossPeakWidth, 1, 1)
layout_.addWidget(self.removeBackgroundToggle, 1, 2)
layout_.addWidget(self.sampleDropdown, 2, 0)
layout_.addWidget(self.groupingFileDropdown, 2, 1)
layout_.addWidget(self.peakFunctionDropdown, 2, 2)
layout_.addWidget(self.liteModeToggle, 0, 2)
layout_.addWidget(self.runFeedbackStateId, 1, 0)
layout_.addWidget(self.runFeedbackRunTitle, 1, 1)
layout_.addWidget(self.skipPixelCalToggle, 1, 2)
layout_.addWidget(self.fieldConvergenceThreshold, 2, 0)
layout_.addWidget(self.fieldNBinsAcrossPeakWidth, 2, 1)
layout_.addWidget(self.removeBackgroundToggle, 2, 2)
layout_.addWidget(self.sampleDropdown, 3, 0)
layout_.addWidget(self.groupingFileDropdown, 3, 1)
layout_.addWidget(self.peakFunctionDropdown, 3, 2)

def populateGroupingDropdown(self, groups):
self.groupingFileDropdown.setItems(groups)

def verify(self):
if not self.runNumberField.text().isdigit():
self._setRunFeedback("", "")
raise ValueError("Please enter a valid run number")
if self.sampleDropdown.currentIndex() < 0:
raise ValueError("Please select a sample")
Expand Down Expand Up @@ -99,3 +122,11 @@ def disablePeakFunction(self):

def enablePeakFunction(self):
self.peakFunctionDropdown.setEnabled(True)

def updateRunFeedback(self, stateId: str, runTitle: str):
self.signalUpdateRunFeedback.emit(stateId, runTitle)

@Slot(str, str)
def _setRunFeedback(self, stateId: str, runTitle: str):
self.runFeedbackStateId.setText(stateId if stateId else "")
self.runFeedbackRunTitle.setText(runTitle if runTitle else "")
36 changes: 33 additions & 3 deletions src/snapred/ui/view/NormalizationRequestView.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from qtpy.QtCore import Signal, Slot

from snapred.meta.decorators.Resettable import Resettable
from snapred.ui.view.BackendRequestView import BackendRequestView

Expand All @@ -15,26 +17,45 @@ class NormalizationRequestView(BackendRequestView):

"""

signalUpdateRunFeedback = Signal(str, str)

def __init__(self, samplePaths=[], groups=[], parent=None):
super(NormalizationRequestView, self).__init__(parent=parent)

self.signalUpdateRunFeedback.connect(self._setRunFeedback)

# input fields
self.runNumberField = self._labeledLineEdit("Run Number:")
self.runNumberField.setToolTip("Vanadium sample run number to be normalized.")
self.liteModeToggle = self._labeledToggle("Lite Mode", True)
self.backgroundRunNumberField = self._labeledLineEdit("Background Run Number:")

self.backgroundRunNumberField.setToolTip("Background run number to be subtracted from the sample run.")
# drop downs
self.sampleDropdown = self._sampleDropDown("Select Sample", samplePaths)
self.sampleDropdown.setToolTip("Samples available for this run number.")
self.groupingFileDropdown = self._sampleDropDown("Select Grouping File", groups)
self.groupingFileDropdown.setToolTip("Grouping schemas available for this sample run number.")

# set field properties
self.liteModeToggle.setEnabled(False)

# run number feedback fields
self.runFeedbackStateId = self._labeledField("State ID")
self.runFeedbackStateId.setToolTip("State ID of the run number.")
self.runFeedbackRunTitle = self._labeledField("Run Title")
self.runFeedbackRunTitle.setToolTip("Title of the run from PV file.")

# run feedback fields are read only
self.runFeedbackStateId.field.setReadOnly(True)
self.runFeedbackRunTitle.field.setReadOnly(True)

# add all widgets to layout
_layout = self.layout()
_layout.addWidget(self.runNumberField, 0, 0)
_layout.addWidget(self.liteModeToggle, 0, 1)
_layout.addWidget(self.backgroundRunNumberField, 1, 0)
_layout.addWidget(self.backgroundRunNumberField, 0, 1)
_layout.addWidget(self.liteModeToggle, 0, 2)
_layout.addWidget(self.runFeedbackStateId, 1, 0)
_layout.addWidget(self.runFeedbackRunTitle, 1, 1)
_layout.addWidget(self.sampleDropdown, 2, 0)
_layout.addWidget(self.groupingFileDropdown, 2, 1)

Expand All @@ -43,6 +64,7 @@ def populateGroupingDropdown(self, groups):

def verify(self):
if not self.runNumberField.text().isdigit():
self._setRunFeedback("", "")
raise ValueError("Please enter a valid run number")
if not self.backgroundRunNumberField.text().isdigit():
raise ValueError("Please enter a valid background run number")
Expand All @@ -62,3 +84,11 @@ def setInteractive(self, flag: bool):
self.liteModeToggle.setEnabled(flag)
self.sampleDropdown.setEnabled(flag)
self.groupingFileDropdown.setEnabled(flag)

def updateRunFeedback(self, stateId: str, runTitle: str):
self.signalUpdateRunFeedback.emit(stateId, runTitle)

@Slot(str, str)
def _setRunFeedback(self, stateId: str, runTitle: str):
self.runFeedbackStateId.setText(stateId if stateId else "")
self.runFeedbackRunTitle.setText(runTitle if runTitle else "")
38 changes: 31 additions & 7 deletions src/snapred/ui/view/reduction/ReductionRequestView.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import abstractmethod
from datetime import datetime, timedelta
from typing import Callable, List, Optional
from typing import Callable, List, Optional, Tuple

from qtpy.QtCore import Qt, QTimer, Signal, Slot
from qtpy.QtGui import QColor
Expand Down Expand Up @@ -46,6 +46,8 @@ def __init__(
self.validateRunNumbers = validateRunNumbers
self.getLiveMetadata = getLiveMetadata

self.retrieveRunFeedbackCallback: Optional[Callable[[str], Tuple[str, str]]] = None

self.runNumbers = []
self.pixelMaskDropdown = self._multiSelectDropDown("Select Pixel Mask(s)", [])

Expand Down Expand Up @@ -78,6 +80,9 @@ def __init__(
self.liteModeToggle.stateChanged.connect(self._populatePixelMaskDropdown)
self.liveDataToggle.stateChanged.connect(self.liveDataModeChange)

def setRetrieveRunFeedbackCallback(self, cb: Callable[[str], Tuple[str, str]]):
self.retrieveRunFeedbackCallback = cb

@ExceptionToErrLog
@Slot(bool)
def _populatePixelMaskDropdown(self, useLiteMode: bool):
Expand Down Expand Up @@ -145,6 +150,8 @@ def getRunNumbers(self):


class _RequestView(_RequestViewBase):
signalUpdateRunFeedback = Signal(str, str)

def __init__(
self,
parent=None,
Expand Down Expand Up @@ -189,6 +196,8 @@ def __init__(
self.enterRunNumberButton.clicked.connect(self.addRunNumber)
self.clearButton.clicked.connect(self.clearRunNumbers)

self.signalUpdateRunFeedback.connect(self._setRunFeedback)

@Slot()
def addRunNumber(self):
try:
Expand All @@ -201,7 +210,11 @@ def addRunNumber(self):
if self.validateRunNumbers is not None:
self.validateRunNumbers(noDuplicates)
self.runNumbers = noDuplicates
self.updateRunNumberList()
if self.retrieveRunFeedbackCallback:
for rn in runNumberList:
stateId, runTitle = self.retrieveRunFeedbackCallback(rn)
lineText = f"{rn}, StateId = {stateId}, Title = {runTitle}"
self.runNumberDisplay.addItem(lineText)
self.runNumberInput.clear()
self._populatePixelMaskDropdown(self.useLiteMode())
except ValueError as e:
Expand Down Expand Up @@ -235,13 +248,22 @@ def parseInputRunNumbers(self) -> List[str]:

def updateRunNumberList(self):
self.runNumberDisplay.clear()
self.runNumberDisplay.addItems(self.runNumbers)

def clearRunNumbers(self):
self.runNumbers.clear()
self.runNumberDisplay.clear()
self.pixelMaskDropdown.setItems([])

def updateRunFeedback(self, stateId: str, runTitle: str):
self.signalUpdateRunFeedback.emit(stateId, runTitle)

@Slot(str, str)
def _setRunFeedback(self, stateId: str, runTitle: str):
if not stateId and not runTitle:
return
itemText = f"StateId={stateId}, Title={runTitle}"
self.runNumberDisplay.addItem(itemText)

###
### Abstract methods:
###
Expand All @@ -251,12 +273,10 @@ def verify(self):
if not runNumbers:
raise ValueError("Please enter at least one run number.")
if runNumbers != self.runNumbers:
raise ValueError("Unexpected issue verifying run numbers. Please clear and re-enter.")
raise ValueError("Unexpected issue verifying run numbers. Please clear and re-enter.")
for runNumber in runNumbers:
if not runNumber.isdigit():
raise ValueError(
"Please enter a valid run number or list of run numbers. (e.g. 46680, 46685, 46686, etc...)"
)
pass
if self.keepUnfocused():
if self.convertUnitsDropdown.currentIndex() < 0:
raise ValueError("Please select units to convert to")
Expand Down Expand Up @@ -628,6 +648,10 @@ def _setLiveDataMode(self, flag: bool):

view.liveDataToggle.toggle.setEnabled(True)

def updateRunFeedback(self, stateId: str, runTitle: str):
if not self.liveDataMode():
self._requestView.updateRunFeedback(stateId, runTitle)

@Slot(LiveMetadata)
def updateLiveMetadata(self, data: LiveMetadata):
if self.liveDataMode():
Expand Down
Loading