Skip to content

Commit 7998d70

Browse files
Pasarusgithub-actions
andauthored
SANs2D rules (#356)
* Add more generic sans2d rules * Add sans2d tests and update loq tests to use generic rules * Add support for mt to rules * Fix ruff linting * Formatting and linting commit * Fix busted extract test * Re-add previously removed mock * Formatting and linting commit * Recalibrate one check by reducing duplication * Formatting and linting commit * Not a parsable experiment title, and log levels changed * Formatting and linting commit --------- Co-authored-by: github-actions <[email protected]>
1 parent 79416c4 commit 7998d70

File tree

12 files changed

+534
-186
lines changed

12 files changed

+534
-186
lines changed

rundetection/ingestion/extracts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def skip_extract(job_request: JobRequest, _: Any) -> JobRequest:
3434
return job_request
3535

3636

37-
def loq_extract(job_request: JobRequest, dataset: Any) -> JobRequest:
37+
def sans_extract(job_request: JobRequest, dataset: Any) -> JobRequest:
3838
"""
3939
Get the sample details and the cycle strings
4040
:param job_request: The job request
@@ -173,8 +173,8 @@ def get_extraction_function(instrument: str) -> Callable[[JobRequest, Any], JobR
173173
return tosca_extract
174174
case "osiris":
175175
return osiris_extract
176-
case "loq":
177-
return loq_extract
176+
case "loq" | "sans2d":
177+
return sans_extract
178178
case "iris":
179179
return iris_extract
180180
case _:

rundetection/rules/common_rules.py

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,53 +28,6 @@ def verify(self, job_request: JobRequest) -> None:
2828
job_request.will_reduce = self._value
2929

3030

31-
class CheckIfScatterSANS(Rule[bool]):
32-
def __init__(self, value: bool):
33-
super().__init__(value)
34-
self.should_be_first = True
35-
36-
def verify(self, job_request: JobRequest) -> None:
37-
if not job_request.experiment_title.endswith("_SANS/TRANS"):
38-
job_request.will_reduce = False
39-
logger.error("Not a scatter run. Does not have _SANS/TRANS at the end of the experiment title.")
40-
return
41-
# If it has empty or direct in the title assume it is a direct run file instead of a normal scatter.
42-
if (
43-
"empty" in job_request.experiment_title
44-
or "EMPTY" in job_request.experiment_title
45-
or "direct" in job_request.experiment_title
46-
or "DIRECT" in job_request.experiment_title
47-
):
48-
job_request.will_reduce = False
49-
logger.error(
50-
"If it is a scatter, contains empty or direct in the title and is assumed to be a scatter "
51-
"for an empty can run."
52-
)
53-
return
54-
if "{" not in job_request.experiment_title and "}" not in job_request.experiment_title:
55-
job_request.will_reduce = False
56-
logger.error("If it is a scatter, contains {} in format {x}_{y}_SANS/TRANS. or {x}_SANS/TRANS.")
57-
return
58-
59-
60-
class SansSliceWavs(Rule[str]):
61-
"""
62-
This rule enables users to set the SliceWavs for each script
63-
"""
64-
65-
def verify(self, job_request: JobRequest) -> None:
66-
job_request.additional_values["slice_wavs"] = self._value
67-
68-
69-
class SansPhiLimits(Rule[str]):
70-
"""
71-
This rule enables users to set the PhiLimits for each script
72-
"""
73-
74-
def verify(self, job_request: JobRequest) -> None:
75-
job_request.additional_values["phi_limits"] = self._value
76-
77-
7831
class MolSpecStitchRule(Rule[bool]):
7932
"""
8033
Enables Tosca, Osiris, and Iris Run stitching

rundetection/rules/factory.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55
from typing import Any
66

77
from rundetection.rules.common_rules import (
8-
CheckIfScatterSANS,
98
EnabledRule,
109
MolSpecStitchRule,
11-
SansPhiLimits,
12-
SansSliceWavs,
1310
)
1411
from rundetection.rules.inter_rules import InterStitchRule
1512
from rundetection.rules.iris_rules import IrisCalibrationRule, IrisReductionRule
16-
from rundetection.rules.loq_rules import LoqFindFiles, LoqUserFile
1713
from rundetection.rules.mari_rules import MariMaskFileRule, MariStitchRule, MariWBVANRule
1814
from rundetection.rules.osiris_rules import (
1915
OsirisDefaultGraniteAnalyser,
@@ -22,6 +18,7 @@
2218
OsirisReflectionCalibrationRule,
2319
)
2420
from rundetection.rules.rule import MissingRuleError, Rule, T
21+
from rundetection.rules.sans_rules import CheckIfScatterSANS, SansFindFiles, SansPhiLimits, SansSliceWavs, SansUserFile
2522

2623

2724
def rule_factory(key_: str, value: T) -> Rule[Any]: # noqa: C901, PLR0911, PLR0912
@@ -65,12 +62,12 @@ def rule_factory(key_: str, value: T) -> Rule[Any]: # noqa: C901, PLR0911, PLR0
6562
case "checkifscattersans":
6663
if isinstance(value, bool):
6764
return CheckIfScatterSANS(value)
68-
case "loqfindfiles":
65+
case "loqfindfiles" | "sansfindfiles":
6966
if isinstance(value, bool):
70-
return LoqFindFiles(value)
71-
case "loquserfile":
67+
return SansFindFiles(value)
68+
case "loquserfile" | "sansuserfile":
7269
if isinstance(value, str):
73-
return LoqUserFile(value)
70+
return SansUserFile(value)
7471
case "sansphilimits":
7572
if isinstance(value, str):
7673
return SansPhiLimits(value)

rundetection/rules/loq_rules.py renamed to rundetection/rules/sans_rules.py

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
1-
"""
2-
Rules for LOQ
3-
"""
4-
51
from __future__ import annotations
62

7-
import logging
83
import re
9-
import typing
104
from dataclasses import dataclass
11-
from pathlib import Path
5+
from typing import TYPE_CHECKING
126

137
import requests
148
import xmltodict
159

10+
from rundetection.rules.common_rules import logger
1611
from rundetection.rules.rule import Rule
1712

18-
if typing.TYPE_CHECKING:
13+
if TYPE_CHECKING:
1914
from rundetection.job_requests import JobRequest
2015

21-
logger = logging.getLogger(__name__)
22-
2316

2417
@dataclass
2518
class SansFileData:
@@ -28,22 +21,23 @@ class SansFileData:
2821
run_number: str
2922

3023

31-
def _extract_run_number_from_filename(filename: str) -> str:
32-
# Assume filename looks like so: LOQ00100002.nxs, then strip.
33-
return filename.split(".")[0].lstrip("LOQ").lstrip("0")
34-
35-
3624
def _is_sample_transmission_file(sans_file: SansFileData, sample_title: str) -> bool:
3725
return sample_title in sans_file.title and sans_file.type == "TRANS"
3826

3927

4028
def _is_sample_direct_file(sans_file: SansFileData) -> bool:
41-
return ("direct" in sans_file.title.lower() or "empty" in sans_file.title.lower()) and sans_file.type == "TRANS"
29+
return (
30+
"direct" in sans_file.title.lower()
31+
or "empty" in sans_file.title.lower()
32+
or "mt " in sans_file.title.lower()
33+
or " mt" in sans_file.title.lower()
34+
or sans_file.title.lower() == "{mt}"
35+
) and sans_file.type == "TRANS"
4236

4337

4438
def _is_can_scatter_file(sans_file: SansFileData, can_title: str) -> bool:
4539
title_contents = re.findall(r"{.*?}", sans_file.title)
46-
return len(title_contents) == 1 and can_title == title_contents[0] and sans_file.type == "SANS/TRANS"
40+
return len(title_contents) == 1 and can_title == title_contents[0] and sans_file.type in {"SANS/TRANS", "SANS"}
4741

4842

4943
def _is_can_transmission_file(sans_file: SansFileData, can_title: str) -> bool:
@@ -79,24 +73,15 @@ def _find_can_trans_file(sans_files: list[SansFileData], can_title: str) -> Sans
7973
return None
8074

8175

82-
def find_path_for_run_number(cycle_path: str, run_number: int) -> Path | None:
83-
# 10 is just a magic number, but we needed an unrealistic value for the maximum
84-
for padding in range(11):
85-
potential_path = Path(f"{cycle_path}/LOQ{str(run_number).zfill(padding)}.nxs")
86-
if potential_path.exists():
87-
return potential_path
88-
return None
89-
90-
91-
def grab_cycle_instrument_index(cycle: str) -> str:
76+
def grab_cycle_instrument_index(cycle: str, instrument: str) -> str:
9277
_, cycle_year, cycle_num = cycle.split("_")
93-
url = f"http://data.isis.rl.ac.uk/journals/ndxloq/journal_{cycle_year}_{cycle_num}.xml"
78+
url = f"http://data.isis.rl.ac.uk/journals/ndx{instrument.lower()}/journal_{cycle_year}_{cycle_num}.xml"
9479
return requests.get(url, timeout=5).text
9580

9681

9782
def create_list_of_files(job_request: JobRequest) -> list[SansFileData]:
9883
cycle = job_request.additional_values["cycle_string"]
99-
xml = grab_cycle_instrument_index(cycle=cycle)
84+
xml = grab_cycle_instrument_index(cycle=cycle, instrument=job_request.instrument)
10085
cycle_run_info = xmltodict.parse(xml)
10186
list_of_files = []
10287
for run_info in cycle_run_info["NXroot"]["NXentry"]:
@@ -121,57 +106,103 @@ def _set_transmission_file(job_request: JobRequest, sample_title: str, sans_file
121106
if not job_request.additional_values["included_trans_as_scatter"]:
122107
trans_file = _find_trans_file(sans_files=sans_files, sample_title=sample_title)
123108
trans_run_number = trans_file.run_number if trans_file is not None else None
124-
logger.info("LOQ trans found %s", trans_run_number)
109+
logger.info("%s trans found %s", job_request.instrument, trans_run_number)
125110
else:
126111
trans_run_number = str(job_request.run_number)
127-
logger.info("LOQ trans set as scatter %s", trans_run_number)
112+
logger.info("%s trans set as scatter %s", job_request.instrument, trans_run_number)
128113
if trans_run_number is not None:
129114
job_request.additional_values["scatter_transmission"] = trans_run_number
130115

131116

132117
def _set_can_files(can_title: str | None, job_request: JobRequest, sans_files: list[SansFileData]) -> None:
133118
if can_title is not None:
134119
can_scatter = _find_can_scatter_file(sans_files=sans_files, can_title=can_title)
135-
logger.info("LOQ can scatter found %s", can_scatter)
120+
logger.info("%s can scatter found %s", job_request.instrument, can_scatter)
136121
if can_scatter is not None:
137122
job_request.additional_values["can_scatter"] = can_scatter.run_number
138123

139124
# If using M4 monitor then can scatter is the transmission
140125
if not job_request.additional_values["included_trans_as_scatter"]:
141126
can_trans = _find_can_trans_file(sans_files=sans_files, can_title=can_title)
142-
logger.info("LOQ can trans found %s", can_trans)
127+
logger.info("%s can trans found %s", job_request.instrument, can_trans)
143128
else:
144129
can_trans = can_scatter
145-
logger.info("LOQ can trans set as scatter %s", can_scatter)
130+
logger.info("%s can trans set as scatter %s", job_request.instrument, can_scatter)
146131
if can_trans is not None and can_scatter is not None:
147132
job_request.additional_values["can_transmission"] = can_trans.run_number
148133

149134

150135
def _set_direct_files(job_request: JobRequest, sans_files: list[SansFileData]) -> None:
151136
direct_file = _find_direct_file(sans_files=sans_files)
152-
logger.info("LOQ direct files found %s", direct_file)
137+
logger.info("%s direct files found %s", job_request.instrument, direct_file)
153138
if direct_file is not None:
154139
if "scatter_transmission" in job_request.additional_values:
155140
job_request.additional_values["scatter_direct"] = direct_file.run_number
156141
if "can_scatter" in job_request.additional_values and "can_transmission" in job_request.additional_values:
157142
job_request.additional_values["can_direct"] = direct_file.run_number
158143

159144

160-
class LoqFindFiles(Rule[bool]):
145+
class CheckIfScatterSANS(Rule[bool]):
146+
def __init__(self, value: bool):
147+
super().__init__(value)
148+
self.should_be_first = True
149+
150+
def verify(self, job_request: JobRequest) -> None:
151+
if not job_request.experiment_title.endswith("_SANS/TRANS") and not job_request.experiment_title.endswith(
152+
"_SANS"
153+
):
154+
job_request.will_reduce = False
155+
logger.info("Not a scatter run. Does not have _SANS or _SANS/TRANS at the end of the experiment title.")
156+
return
157+
# If it is a direct fix, sans or trans, it should fail, which is why hard coded TRANS as we want to check
158+
# part of the logic not all.
159+
if _is_sample_direct_file(
160+
SansFileData(title=job_request.experiment_title, type="TRANS", run_number=str(job_request.run_number))
161+
):
162+
job_request.will_reduce = False
163+
logger.info("File is an empty cell or direct beam scatter run, and should not be processed")
164+
return
165+
if "{" not in job_request.experiment_title and "}" not in job_request.experiment_title:
166+
job_request.will_reduce = False
167+
logger.info(
168+
"Not a parsable scatter title, a scatter contains {} in format {x}_{y}_SANS/TRANS. or {x}_SANS/TRANS."
169+
)
170+
return
171+
172+
173+
class SansSliceWavs(Rule[str]):
174+
"""
175+
This rule enables users to set the SliceWavs for each script
176+
"""
177+
178+
def verify(self, job_request: JobRequest) -> None:
179+
job_request.additional_values["slice_wavs"] = self._value
180+
181+
182+
class SansPhiLimits(Rule[str]):
183+
"""
184+
This rule enables users to set the PhiLimits for each script
185+
"""
186+
187+
def verify(self, job_request: JobRequest) -> None:
188+
job_request.additional_values["phi_limits"] = self._value
189+
190+
191+
class SansFindFiles(Rule[bool]):
161192
def __init__(self, value: bool):
162193
super().__init__(value)
163194
self._should_be_last = True
164195

165196
def verify(self, job_request: JobRequest) -> None:
166197
title = job_request.experiment_title
167-
logger.info("LOQ title is %s", title)
198+
logger.info("%s title is %s", job_request.instrument, title)
168199
# Find all of the "titles" [0] is the scatter, [1] is the background
169200
title_parts = re.findall(r"{.*?}", title)
170201
sample_title = title_parts[0]
171-
logger.info("LOQ sample title is %s", sample_title)
202+
logger.info("%s sample title is %s", job_request.instrument, sample_title)
172203
# If background was defined in the title set can title
173204
can_title = title_parts[1] if len(title_parts) > 1 else None
174-
logger.info("LOQ can title is %s from list %s", can_title, title_parts)
205+
logger.info("%s can title is %s from list %s", job_request.instrument, can_title, title_parts)
175206

176207
# Get the file lists
177208
sans_files = create_list_of_files(job_request)
@@ -188,8 +219,8 @@ def verify(self, job_request: JobRequest) -> None:
188219
_set_direct_files(job_request, sans_files)
189220

190221

191-
class LoqUserFile(Rule[str]):
222+
class SansUserFile(Rule[str]):
192223
def verify(self, job_request: JobRequest) -> None:
193224
# If M4 in user file then the transmission and scatter files are the same.
194225
job_request.additional_values["included_trans_as_scatter"] = "_M4" in self._value
195-
job_request.additional_values["user_file"] = f"/extras/loq/{self._value}"
226+
job_request.additional_values["user_file"] = f"/extras/{job_request.instrument.lower()}/{self._value}"

test/ingestion/test_extracts.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
from rundetection.ingestion.extracts import (
1212
get_cycle_string_from_path,
1313
get_extraction_function,
14-
loq_extract,
1514
mari_extract,
1615
osiris_extract,
16+
sans_extract,
1717
skip_extract,
1818
tosca_extract,
1919
)
@@ -60,7 +60,8 @@ def test_skip_extract(caplog: LogCaptureFixture):
6060
("mari", "mari_extract"),
6161
("tosca", "tosca_extract"),
6262
("osiris", "osiris_extract"),
63-
("loq", "loq_extract"),
63+
("loq", "sans_extract"),
64+
("sans2d", "sans_extract"),
6465
],
6566
)
6667
def test_get_extraction_function(input_value, expected_function_name):
@@ -238,7 +239,7 @@ def test_osiris_extract_raises_on_bad_frequencies(job_request):
238239
osiris_extract(job_request, dataset)
239240

240241

241-
def test_loq_extract(job_request):
242+
def test_sans_extract(job_request):
242243
dataset = {
243244
"sample": {
244245
"thickness": [1.0],
@@ -248,7 +249,7 @@ def test_loq_extract(job_request):
248249
}
249250
}
250251
with patch("rundetection.ingestion.extracts.get_cycle_string_from_path", return_value="some string"):
251-
loq_extract(job_request, dataset)
252+
sans_extract(job_request, dataset)
252253

253254
assert job_request.additional_values["cycle_string"] == "some string"
254255
assert job_request.additional_values["sample_thickness"] == 1.0

test/rules/test_common_rules.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010

1111
from rundetection.ingestion.ingest import JobRequest
1212
from rundetection.rules.common_rules import (
13-
CheckIfScatterSANS,
1413
EnabledRule,
15-
SansPhiLimits,
16-
SansSliceWavs,
1714
is_y_within_5_percent_of_x,
1815
)
16+
from rundetection.rules.sans_rules import CheckIfScatterSANS, SansPhiLimits, SansSliceWavs
1917

2018

2119
@pytest.fixture
@@ -49,7 +47,7 @@ def test_enabled_rule_when_not_enabled(job_request) -> None:
4947
assert job_request.will_reduce is False
5048

5149

52-
@pytest.mark.parametrize("end_of_title", ["_TRANS", "_SANS", "COOL", "_sans/trans"])
50+
@pytest.mark.parametrize("end_of_title", ["_TRANS", "COOL", "_sans/trans"])
5351
def test_checkifscattersans_verify_raises_for_no_sans_trans(end_of_title) -> None:
5452
job_request = mock.MagicMock()
5553
job_request.experiment_title = "{fancy chemical}" + end_of_title
@@ -62,6 +60,8 @@ def test_checkifscattersans_verify_raises_for_no_sans_trans(end_of_title) -> Non
6260
def test_checkifscattersans_verify_raises_for_direct_or_empty_in_title(to_raise) -> None:
6361
job_request = mock.MagicMock()
6462
job_request.experiment_title = "{fancy chemical " + to_raise + "}_SANS/TRANS"
63+
job_request.will_reduce = True
64+
job_request.run_number = 223312
6565
CheckIfScatterSANS(True).verify(job_request)
6666

6767
assert job_request.will_reduce is False

0 commit comments

Comments
 (0)