diff --git a/docs/explanations/beamlines.md b/docs/explanations/beamlines.md new file mode 100644 index 00000000..47e64241 --- /dev/null +++ b/docs/explanations/beamlines.md @@ -0,0 +1,8 @@ +# Beamlines + +```{toctree} +:maxdepth: 1 +:glob: + +i15-1 +``` diff --git a/docs/explanations/beamlines/i15-1/auxiliary_scans.md b/docs/explanations/beamlines/i15-1/auxiliary_scans.md new file mode 100644 index 00000000..adf40b41 --- /dev/null +++ b/docs/explanations/beamlines/i15-1/auxiliary_scans.md @@ -0,0 +1,25 @@ +# i15-1 Auxiliary Scans + +I15-1 PDF experiments require some scans to be run before hand, for use during analysis. These are added to the queue automatically by the i15-1 queue plugin. For each experiment, we need: + +- an air scan, at room temperature +- an empty capillary scan, matching the capillary type and temperature range to the experiment +- a standard sample scan, at room temperature, matching the capillary type to the experiment + +These must happen in this order, as the analysis process is layered - i.e, the empty capillary scan needs the air scan to be processed, the standard sample needs the empty capillary, and therefore the air, and the actual experiment needs all three. Additionally, to save time, we can merge these auxiliary scans together, to avoid queuing up three additional scans for every experiment. For example, experiments can always share an air scan (as it's always the same); empty capillary scans can be shared as long as the temperature range spans the range of the experiment, and the capillary type is the same; and standard sample scans can be shared as long as the capillary type is the same. + +The above requirements lead to the following logic: + +- An experiment will require the three auxiliary scans outlined above, the details of which will be a function of the experiment parameters. See `I151Converter._get_required_auxiliary_scans` +- An `AuxiliaryScan` has a `requires` field, which details the other required auxiliary scans that must proceed it. The `AuxiliaryScan` objects produced by `I151Converter._get_required_auxiliary_scans` have each other as requirements. The empty capillary scan will be required by the standard sample scan, and the air scan will be required by both the other two. + +To add auxiliary scans, we build a new queue that includes the tasks along with their auxiliary scans. For each task in the queue, we will: + +- Gather the required auxiliary scans +- For each required auxiliary scan: +- Check whether a matching auxiliary scan already exists in the queue or tiled. For an existing scan to be considered suitable, all of the required auxiliary scan's own required auxiliary scans must also be required by the existing scan. +- If a suitable existing does not exist, we will see if we can combine the required auxiliary scan with an existing scan. This, for example, could involve expanding the temperature range of a queued auxiliary scan. Combining these auxiliary scans also combines the requirements of both scans, so no requirement information is lost, but we also don't duplicate similar requirements. We then go to the beginning of this checklist, and repeat for the required auxiliary scans of the combined auxiliary scan, inserting the results before the combined scan in the queue. +- If we cannot combine the required auxiliary scan, we will add the auxiliary scan to the queue. However, right before we do this, we will go through the required auxiliary scans of the auxiliary scan we added, and repeat this checklist for them. This ensures the auxiliary scan is added behind its requirements. +- Once all the recursive adding of required auxiliary scans is complete, we add the task itself to the new queue. + +![Auxiliary scan logic flowchart](./i15-1_aux_scans.drawio.png) diff --git a/docs/explanations/beamlines/i15-1/backgrounds_logic.drawio.png b/docs/explanations/beamlines/i15-1/backgrounds_logic.drawio.png deleted file mode 100644 index 8670a22c..00000000 Binary files a/docs/explanations/beamlines/i15-1/backgrounds_logic.drawio.png and /dev/null differ diff --git a/docs/explanations/beamlines/i15-1/i15-1.md b/docs/explanations/beamlines/i15-1/i15-1.md new file mode 100644 index 00000000..63aaf926 --- /dev/null +++ b/docs/explanations/beamlines/i15-1/i15-1.md @@ -0,0 +1,8 @@ +# i15-1 + +```{toctree} +:maxdepth: 1 +:glob: + +Auxiliary Scans +``` diff --git a/docs/explanations/beamlines/i15-1/i15-1_aux_scans.drawio.png b/docs/explanations/beamlines/i15-1/i15-1_aux_scans.drawio.png new file mode 100644 index 00000000..a94aae81 Binary files /dev/null and b/docs/explanations/beamlines/i15-1/i15-1_aux_scans.drawio.png differ diff --git a/docs/reference/rest_api.json b/docs/reference/rest_api.json index c93877bf..0785aa54 100644 --- a/docs/reference/rest_api.json +++ b/docs/reference/rest_api.json @@ -1072,29 +1072,9 @@ "OIDCConfig": { "properties": { "well_known_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "type": "string", "title": "Well Known Url", - "description": "URL to fetch OIDC config from the provider", - "deprecated": true - }, - "issuer": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Issuer", - "description": "URL of OIDC provider" + "description": "URL to fetch OIDC config from the provider" }, "client_id": { "type": "string", @@ -1117,6 +1097,7 @@ "additionalProperties": false, "type": "object", "required": [ + "well_known_url", "client_id" ], "title": "OIDCConfig" diff --git a/src/daq_queuing_service/external_interaction/tiled/tiled.py b/src/daq_queuing_service/external_interaction/tiled/tiled.py index 3e3b7b96..40966a7a 100644 --- a/src/daq_queuing_service/external_interaction/tiled/tiled.py +++ b/src/daq_queuing_service/external_interaction/tiled/tiled.py @@ -55,16 +55,19 @@ def get_tiled_client( def get_metadata_from_tiled( - tiled_client: Container, instrument_session: str, blueapi_task_id: str + tiled_client: Container, + instrument: str, + instrument_session: str, + blueapi_task_id: str, ) -> list[tuple[str, dict[str, Any]]]: @cached(cache) def _query_tiled( - instrument_session: str, blueapi_task_id: str + instrument: str, instrument_session: str, blueapi_task_id: str ) -> list[tuple[str, dict[str, Any]]]: - result: Container = tiled_client.search( - Eq("start.instrument_session", instrument_session) - ).search(Eq("start.blueapi_task_id", blueapi_task_id)) + result: Container = tiled_client[ + f"{instrument}/{instrument_session.split('-')[0]}/{instrument_session}" + ].search(Eq("start.blueapi_task_id", blueapi_task_id)) items = sorted( ((key, dict(value.metadata)) for key, value in result.items()), @@ -72,10 +75,10 @@ def _query_tiled( ) return items - return _query_tiled(instrument_session, blueapi_task_id) + return _query_tiled(instrument, instrument_session, blueapi_task_id) -def get_tiled_and_scan_ids(tiled_client: Container, call: BlueapiCall): +def get_tiled_and_scan_ids(tiled_client: Container, instrument: str, call: BlueapiCall): tiled_ids: list[str] = [] scan_ids: list[int] = [] @@ -84,7 +87,7 @@ def get_tiled_and_scan_ids(tiled_client: Container, call: BlueapiCall): return tiled_ids, scan_ids scan_metadatas = get_metadata_from_tiled( - tiled_client, call.task_request.instrument_session, call.blueapi_id + tiled_client, instrument, call.task_request.instrument_session, call.blueapi_id ) for item in scan_metadatas: diff --git a/src/daq_queuing_service/plugins/i15_1/auxiliary.py b/src/daq_queuing_service/plugins/i15_1/auxiliary.py index 061336fd..94a2ab45 100644 --- a/src/daq_queuing_service/plugins/i15_1/auxiliary.py +++ b/src/daq_queuing_service/plugins/i15_1/auxiliary.py @@ -1,8 +1,10 @@ +from collections.abc import Sequence from enum import StrEnum from pathlib import Path +from typing import Literal, Self from daq_config_server.models.i15_1.standards_puck import StandardsPin -from pydantic import BaseModel, ConfigDict, computed_field +from pydantic import BaseModel, ConfigDict, Field, computed_field from daq_queuing_service.task_queue.task import Experiment @@ -32,6 +34,28 @@ def get_background_temperatures( ) +def combine_auxiliary_requirements( + required_auxiliaries_1: Sequence["AuxiliaryScan"], + required_auxiliaries_2: list["AuxiliaryScan"], +) -> list["AuxiliaryScan"]: + result: list[AuxiliaryScan] = [] + remaining = required_auxiliaries_2.copy() + for aux1 in required_auxiliaries_1: + for aux2 in remaining: + if combined := aux1.attempt_to_combine_with(aux2): + result.append(combined) + remaining.remove(aux2) + + break + else: + result.append(aux1) + + return result + remaining + + +Q_MAX_OPTIONS = Literal[25, 30, 35, 40] + + class AuxiliaryScan(BaseModel): """Auxiliary scans are scans added automatically by the queue, such as background scans and standard sample calibration scans. They are needed for analysis of user @@ -42,7 +66,9 @@ class AuxiliaryScan(BaseModel): instrument_session: str pin: StandardsPin | None time_per_pdf: float + q_max: Q_MAX_OPTIONS list_of_temperatures: list[int] | None = None + requires: list[Self] = Field(default_factory=lambda: [], exclude=True) @computed_field @property @@ -59,7 +85,9 @@ def is_suitable( temperature_step: int = DEFAULT_TEMPERATURE_STEP, ) -> bool: """Determine if this background is suitable compared to an experiment's required - background. + background. This includes checking that scans in required_background.requires + have a suitable pairing in self.requires, to ensure nested requirements are + taken into account. Args: required_background (AuxiliaryScan): The required background @@ -75,10 +103,15 @@ def is_suitable( elif self.list_of_temperatures: return False + for required in required_background.requires: + if not any(r.is_suitable(required) for r in self.requires): + return False + return ( self.instrument_session == required_background.instrument_session and self.pin == required_background.pin and self.time_per_pdf >= required_background.time_per_pdf + and self.q_max >= required_background.q_max ) def _temperatures_match( @@ -109,7 +142,8 @@ def attempt_to_combine_with( temperature_step: int = DEFAULT_TEMPERATURE_STEP, ) -> "AuxiliaryScan | None": """Creates a background that combines the requirements of this background object - and a provided required background, if possible. + and a provided required background, if possible. Anything that previously deemed + this scan as suitable should also deem the combined scan suitable. Args: required_background (AuxiliaryScan): The required background @@ -142,6 +176,10 @@ def attempt_to_combine_with( pin=self.pin, time_per_pdf=max(self.time_per_pdf, required_background.time_per_pdf), list_of_temperatures=list_of_temperatures, + requires=combine_auxiliary_requirements( + self.requires, required_background.requires + ), + q_max=max(self.q_max, required_background.q_max), ) assert result.is_suitable(required_background) return result @@ -164,9 +202,13 @@ def from_experiment(cls, experiment: Experiment) -> "AuxiliaryScan": instrument_session=experiment.instrument_session, pin=pin, time_per_pdf=experiment.experiment_definition.data["time_per_pdf"], + q_max=experiment.experiment_definition.data["q_max"], list_of_temperatures=experiment.experiment_definition.data.get( "list_of_temperatures" ), + requires=experiment.experiment_definition.data.get( + "required_auxiliary_scans", [] + ), ) diff --git a/src/daq_queuing_service/plugins/i15_1/i15_1_converter.py b/src/daq_queuing_service/plugins/i15_1/i15_1_converter.py index 5819c899..6c753138 100644 --- a/src/daq_queuing_service/plugins/i15_1/i15_1_converter.py +++ b/src/daq_queuing_service/plugins/i15_1/i15_1_converter.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, replace from functools import cached_property from typing import Any @@ -52,9 +53,16 @@ ) -def _filter_auxiliary_scans(tasks: list[Task]) -> list[tuple[int, AuxiliaryScan]]: +@dataclass(frozen=True) +class QueuedAuxiliary: + index: int + task_id: str + auxiliary: AuxiliaryScan + + +def _filter_auxiliary_scans(tasks: list[Task]) -> list[QueuedAuxiliary]: return [ - (i, AuxiliaryScan.from_experiment(task.experiment)) + QueuedAuxiliary(i, task.id, AuxiliaryScan.from_experiment(task.experiment)) for i, task in enumerate(tasks) if isinstance(task.experiment, Experiment) and is_auxiliary_experiment(task.experiment) @@ -165,7 +173,7 @@ def _construct_blueapi_tasks_from_experiment( if self._queue_params.do_wait_for_beam: plans.append(get_wait_for_beam(session)) - if sample and self._queue_params.do_centring: + if sample and sample.data.get("composition") and self._queue_params.do_centring: plans.append(get_centre_sample(centring_params, experiment)) if self._queue_params.do_wait_for_beam: plans.append(get_wait_for_beam(session)) @@ -200,9 +208,7 @@ def _add_required_auxiliary_scans( for task in tasks: with add_session_to_logger(task.experiment.instrument_session): experiment = task.experiment - if isinstance(experiment, Experiment) and not is_auxiliary_experiment( - experiment - ): + if isinstance(experiment, Experiment): instrument_session = experiment.instrument_session required_auxiliary_scans = self._get_required_auxiliary_scans( @@ -241,8 +247,8 @@ def _ensure_auxiliary_in_queue_or_tiled( queued_auxiliary_scans = _filter_auxiliary_scans(new_tasks) if any( - queued_auxiliary.is_suitable(required_auxiliary) - for _, queued_auxiliary in queued_auxiliary_scans + queued_auxiliary.auxiliary.is_suitable(required_auxiliary) + for queued_auxiliary in queued_auxiliary_scans ): return new_tasks @@ -260,6 +266,7 @@ def _ensure_auxiliary_in_queue_or_tiled( ) return self._add_or_replace_auxiliary_scan( required_auxiliary, + current_task, new_tasks, queued_auxiliary_scans, instrument_session, @@ -268,33 +275,68 @@ def _ensure_auxiliary_in_queue_or_tiled( def _add_or_replace_auxiliary_scan( self, required_auxiliary: AuxiliaryScan, + current_task: TaskWithPosition | None, new_tasks: list[Task], - queued_auxiliary_scans: list[tuple[int, AuxiliaryScan]], + queued_auxiliary_scans: list[QueuedAuxiliary], instrument_session: str, ) -> list[Task]: - combined_auxiliary_scan = None - index = None - - for i, auxiliary_scan in queued_auxiliary_scans: - if combined_auxiliary_scan := auxiliary_scan.attempt_to_combine_with( - required_auxiliary - ): - index = i - break + # It is assumed every scan in queued_auxiliary_scans has already added their + # requirements before for them + combined_auxiliary = self._find_combinable_auxiliary( + required_auxiliary, queued_auxiliary_scans + ) aux_experiment = self._construct_auxiliary_experiment( - combined_auxiliary_scan or required_auxiliary, instrument_session + combined_auxiliary.auxiliary if combined_auxiliary else required_auxiliary, + instrument_session, ) - if index is None: - new_tasks.append(Task(experiment=aux_experiment)) + + if combined_auxiliary is None: + aux_task = Task(experiment=aux_experiment) + prefix = new_tasks + suffix = [] + additional_required = required_auxiliary.requires else: - new_tasks[index] = Task(experiment=aux_experiment) + aux_task = Task(experiment=aux_experiment, id=combined_auxiliary.task_id) + prefix = new_tasks[: combined_auxiliary.index] + suffix = new_tasks[combined_auxiliary.index + 1 :] + additional_required = combined_auxiliary.auxiliary.requires + + self._tiled_auxiliary_scans[aux_task.id] = {} + for required in additional_required: + prefix = self._ensure_auxiliary_in_queue_or_tiled( + required, + current_task, + prefix, + aux_task.id, + instrument_session, + ) + new_tasks = prefix + [aux_task] + suffix + return new_tasks + def _find_combinable_auxiliary( + self, + required_auxiliary: AuxiliaryScan, + queued_auxiliary_scans: list[QueuedAuxiliary], + ): + for queued_auxiliary in queued_auxiliary_scans: + if ( + combined_auxiliary_scan + := queued_auxiliary.auxiliary.attempt_to_combine_with( + required_auxiliary + ) + ): + return replace(queued_auxiliary, auxiliary=combined_auxiliary_scan) + def _get_required_auxiliary_scans( self, experiment: Experiment ) -> list[AuxiliaryScan]: + if is_auxiliary_experiment(experiment): + return experiment.experiment_definition.data["required_auxiliary_scans"] + time_per_pdf = experiment.experiment_definition.data["time_per_pdf"] + q_max = experiment.experiment_definition.data["q_max"] list_of_temperatures = experiment.experiment_definition.data.get( "list_of_temperatures" ) @@ -302,34 +344,44 @@ def _get_required_auxiliary_scans( list_of_temperatures = get_background_temperatures(list_of_temperatures) assert experiment.sample - return [ - AuxiliaryScan( - instrument_session=experiment.instrument_session, - pin=None, - time_per_pdf=time_per_pdf, - ), - AuxiliaryScan( - instrument_session=experiment.instrument_session, - pin=StandardsPin( - capillary=experiment.sample.data["capillary"], contents=None - ), - time_per_pdf=time_per_pdf, - list_of_temperatures=list_of_temperatures, + + air = AuxiliaryScan( + instrument_session=experiment.instrument_session, + pin=None, + time_per_pdf=time_per_pdf, + q_max=q_max, + ) + empty_capillary = AuxiliaryScan( + instrument_session=experiment.instrument_session, + pin=StandardsPin( + capillary=experiment.sample.data["capillary"], contents=None ), - AuxiliaryScan( - instrument_session=experiment.instrument_session, - pin=StandardsPin( - capillary=experiment.sample.data["capillary"], contents="Silicon" - ), - time_per_pdf=time_per_pdf, + time_per_pdf=time_per_pdf, + q_max=q_max, + list_of_temperatures=list_of_temperatures, + requires=[air], + ) + standard_sample = AuxiliaryScan( + instrument_session=experiment.instrument_session, + pin=StandardsPin( + capillary=experiment.sample.data["capillary"], contents="Silicon" ), - ] + time_per_pdf=time_per_pdf, + q_max=q_max, + requires=[air, empty_capillary], + ) + + return [standard_sample, empty_capillary, air] def _construct_auxiliary_experiment( self, auxiliary_scan: AuxiliaryScan, instrument_session: str ) -> Experiment: LOGGER.debug(f"Constructing experiment for auxiliary scan: {auxiliary_scan}") - data: dict[str, Any] = {"time_per_pdf": auxiliary_scan.time_per_pdf} + data: dict[str, Any] = { + "time_per_pdf": auxiliary_scan.time_per_pdf, + "q_max": auxiliary_scan.q_max, + "required_auxiliary_scans": auxiliary_scan.requires, + } if auxiliary_scan.list_of_temperatures: settle_time = ( self._queue_params.blower_auxiliary_collection_params.settle_time_s diff --git a/src/daq_queuing_service/plugins/i15_1/plans.py b/src/daq_queuing_service/plugins/i15_1/plans.py index 3942268a..c2ce2440 100644 --- a/src/daq_queuing_service/plugins/i15_1/plans.py +++ b/src/daq_queuing_service/plugins/i15_1/plans.py @@ -105,6 +105,7 @@ def get_data_collection( "exposure_time_per_frame": data_collection_params.exposure_time_per_frame_s, "scan_type": scan_type, "metadata": collection_metadata, + "q_max": experiment_definition.data["q_max"], } if experiment_is_blower(experiment): diff --git a/src/daq_queuing_service/plugins/i15_1/tiled_interaction.py b/src/daq_queuing_service/plugins/i15_1/tiled_interaction.py index 9e00504a..9ac159be 100644 --- a/src/daq_queuing_service/plugins/i15_1/tiled_interaction.py +++ b/src/daq_queuing_service/plugins/i15_1/tiled_interaction.py @@ -31,67 +31,32 @@ def _query_tiled(instrument_session: str) -> list[TiledAuxiliary]: oldest_valid_time = time.time() - stale_time_s result: Container = ( - tiled_client.search(Eq("start.instrument", "i15-1")) - .search(Eq("start.instrument_session", instrument_session)) + tiled_client[ + f"i15-1/{instrument_session.split('-')[0]}/{instrument_session}" + ] .search(Eq("stop.exit_status", "success")) .search(Comparison("ge", "stop.time", oldest_valid_time)) .search(In("start.scan_type", list(AuxiliaryScanType))) .search(KeyPresent("start.experiment_definition.data.time_per_pdf")) + .search(KeyPresent("start.experiment_definition.data.q_max")) ) - items = sorted( - ((key, value) for key, value in result.items()), - key=lambda item: item[1].metadata["start"]["time"], - reverse=True, - ) + tiled_ids = [ + item[0] + for item in sorted( + result.items(), + key=lambda item: item[1].metadata["start"]["time"], + reverse=True, + ) + ] auxiliary_scans: list[TiledAuxiliary] = [] - for item in items: - tiled_id = item[0] - start_doc = item[1].metadata["start"] - filename = f"{start_doc['scan_file']}.nxs" - instrument_session_directory = Path(start_doc["data_session_directory"]) - filepath = instrument_session_directory / filename - - sample = start_doc.get("sample_info") - - if sample is None: - pin = None - else: - capillary = sample["data"].get("capillary") - if not capillary: - LOGGER.warning( - f"No capillary found for tiled scan '{tiled_id}'. Skipping." - ) - continue - pin = StandardsPin( - capillary=sample["data"]["capillary"], - contents=sample["data"].get("composition"), - ) - time_per_pdf = start_doc["experiment_definition"]["data"]["time_per_pdf"] - list_of_temperatures = start_doc["experiment_definition"]["data"].get( - "list_of_temperatures" - ) - - auxiliary_scan = TiledAuxiliary( - tiled_id=tiled_id, - instrument_session=instrument_session, - filename=filename, - instrument_session_directory=instrument_session_directory, - filepath=filepath, - pin=pin, - time_per_pdf=time_per_pdf, - list_of_temperatures=list_of_temperatures, - ) - if auxiliary_scan.kind != start_doc["scan_type"]: - LOGGER.warning( - f"Inferred auxiliary type: '{auxiliary_scan.kind}' does not match " - + f"scan type in metadata: '{start_doc['scan_type']}' for auxiliary" - + f" scan '{tiled_id}'. Skipping." - ) - continue - auxiliary_scans.append(auxiliary_scan) + for tiled_id in tiled_ids: + if auxiliary_scan := construct_tiled_auxiliary( + result, tiled_id, instrument_session + ): + auxiliary_scans.append(auxiliary_scan) LOGGER.debug( f"Found {len(auxiliary_scans)} auxiliary scans in tiled since " @@ -107,3 +72,59 @@ def _query_tiled(instrument_session: str) -> list[TiledAuxiliary]: LOGGER.info( f"Found no suitable auxiliary scans in tiled matching: {required_scan}." ) + + +def construct_tiled_auxiliary( + container: Container, tiled_id: str, instrument_session: str +) -> TiledAuxiliary | None: + start_doc = container[tiled_id].metadata["start"] + filename = f"{start_doc['scan_file']}.nxs" + instrument_session_directory = Path(start_doc["data_session_directory"]) + filepath = instrument_session_directory / filename + + sample = start_doc.get("sample_info") + + if sample is None: + pin = None + else: + capillary = sample["data"].get("capillary") + if not capillary: + LOGGER.warning(f"No capillary found for tiled scan '{tiled_id}'. Skipping.") + return + pin = StandardsPin( + capillary=sample["data"]["capillary"], + contents=sample["data"].get("composition"), + ) + time_per_pdf = start_doc["experiment_definition"]["data"]["time_per_pdf"] + q_max = start_doc["experiment_definition"]["data"]["q_max"] + list_of_temperatures = start_doc["experiment_definition"]["data"].get( + "list_of_temperatures" + ) + + requires = [ + construct_tiled_auxiliary( + container, auxiliary_scan["tiled_id"], instrument_session + ) + for auxiliary_scan in start_doc.get("auxiliary_scans", {}).values() + ] + + auxiliary_scan = TiledAuxiliary( + tiled_id=tiled_id, + instrument_session=instrument_session, + filename=filename, + instrument_session_directory=instrument_session_directory, + filepath=filepath, + pin=pin, + time_per_pdf=time_per_pdf, + q_max=q_max, + list_of_temperatures=list_of_temperatures, + requires=[scan for scan in requires if scan is not None], + ) + if auxiliary_scan.kind != start_doc["scan_type"]: + LOGGER.warning( + f"Inferred auxiliary type: '{auxiliary_scan.kind}' does not match " + + f"scan type in metadata: '{start_doc['scan_type']}' for auxiliary" + + f" scan '{tiled_id}'. Skipping." + ) + return + return auxiliary_scan diff --git a/src/daq_queuing_service/task_queue/queue.py b/src/daq_queuing_service/task_queue/queue.py index 4894d1fa..364d1b74 100644 --- a/src/daq_queuing_service/task_queue/queue.py +++ b/src/daq_queuing_service/task_queue/queue.py @@ -354,7 +354,11 @@ async def complete_call(self, call: BlueapiCall, result: TaskResult): result (TaskResult): The result of the task from blueapi """ with add_session_to_logger(call.task_request.instrument_session): - tiled_ids, scan_ids = get_tiled_and_scan_ids(self._tiled_client, call) + tiled_ids, scan_ids = ( + get_tiled_and_scan_ids(self._tiled_client, self.instrument, call) + if self.instrument + else ([], []) + ) should_pause = self._converter.pause_on_complete_blueapi_call( call.model_copy(), result ) @@ -383,7 +387,12 @@ async def fail_call( should_pause = self._converter.pause_on_fail_blueapi_call( call.model_copy(), errors ) - tiled_ids, scan_ids = get_tiled_and_scan_ids(self._tiled_client, call) + + tiled_ids, scan_ids = ( + get_tiled_and_scan_ids(self._tiled_client, self.instrument, call) + if self.instrument + else ([], []) + ) LOGGER.error(f"Plan {call} has failed with the following errors: {errors}") async with self._modifying: diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index bae0ca47..652422ae 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -59,11 +59,14 @@ def setup_logger_for_tests(): CONTAINER = Container(id="", positionInParent=CONTAINER_POSITION) -def make_sample(name: str, id: str): +def make_sample(name: str, id: str, composition: str = ""): + data = {"capillary": "fq1.0", "composition": "chemical"} + if composition: + data["composition"] = composition return Sample( name=name, id=id, - data={"capillary": "fq1.0"}, + data=data, positionInContainer=CONTAINER_POSITION, container=CONTAINER, ) @@ -79,7 +82,7 @@ def tasks() -> list[Task]: experiment_definition=ExperimentDefinition( name="test", id=str(i), data={} ), - sample=make_sample(f"test_8_{i}", str(i)), + sample=make_sample(f"test_8_{i}", str(i), composition="chemical"), ), id=str(i), ) diff --git a/tests/unit_tests/external_interaction/tiled/test_tiled.py b/tests/unit_tests/external_interaction/tiled/test_tiled.py index 7009797a..4c4c137e 100644 --- a/tests/unit_tests/external_interaction/tiled/test_tiled.py +++ b/tests/unit_tests/external_interaction/tiled/test_tiled.py @@ -39,14 +39,13 @@ def mock_tiled_search( } } - search_result = MagicMock() - search_result.search = MagicMock( + search_result = MagicMock( return_value={"tiled_id_1": result_1, "tiled_id_2": result_2} ) - tiled_client.search = MagicMock(return_value=search_result) + tiled_client["ixx/cm12345/cm12345-1"].search = search_result - return (tiled_client, search_result) + return tiled_client def test_get_tiled_client_instantiates_client_from_uri(): @@ -104,22 +103,21 @@ def get_next_env_variable(*_, **__): # type:ignore ) -def test_get_metadata_from_tiled_makes_expected_searches( - mock_tiled_search: tuple[MagicMock, MagicMock], +def test_get_metadata_from_tiled_makes_expected_search( + mock_tiled_search: MagicMock, ): - client, result = mock_tiled_search - get_metadata_from_tiled(client, "cm12345-1", "bapi_task_id") - client.search.assert_called_once_with( - Eq(key="start.instrument_session", value="cm12345-1") + client = mock_tiled_search + get_metadata_from_tiled(client, "ixx", "cm12345-1", "bapi_task_id") + client["ixx/cm12345/cm12345-1"].search.assert_called_once_with( # type:ignore + Eq("start.blueapi_task_id", "bapi_task_id") ) - result.search.assert_called_once_with(Eq("start.blueapi_task_id", "bapi_task_id")) def test_get_metadata_from_tiled_returns_expected_result( - mock_tiled_search: tuple[MagicMock, MagicMock], + mock_tiled_search: MagicMock, ): - client, _ = mock_tiled_search - result = get_metadata_from_tiled(client, "cm12345-1", "bapi_task_id") + client = mock_tiled_search + result = get_metadata_from_tiled(client, "ixx", "cm12345-1", "bapi_task_id") assert result == [ # Should be in chronological order ( "tiled_id_2", @@ -149,9 +147,9 @@ def test_get_metadata_from_tiled_returns_expected_result( def test_get_metadata_from_tiled_returns_empty_list_if_no_result_found( - mock_tiled_search: tuple[MagicMock, MagicMock], + mock_tiled_search: MagicMock, ): - client, search = mock_tiled_search - search.search = MagicMock(return_value={}) - result = get_metadata_from_tiled(client, "cm12345-1", "bapi_task_id") + client = mock_tiled_search + client["ixx/cm12345/cm12345-1"].search = MagicMock(return_value={}) + result = get_metadata_from_tiled(client, "ixx", "cm12345-1", "bapi_task_id") assert result == [] diff --git a/tests/unit_tests/plugins/i15_1/test_auxiliary.py b/tests/unit_tests/plugins/i15_1/test_auxiliary.py index e4784002..99e993f8 100644 --- a/tests/unit_tests/plugins/i15_1/test_auxiliary.py +++ b/tests/unit_tests/plugins/i15_1/test_auxiliary.py @@ -1,7 +1,10 @@ import pytest from daq_config_server.models.i15_1.standards_puck import StandardsPin -from daq_queuing_service.plugins.i15_1.auxiliary import AuxiliaryScan +from daq_queuing_service.plugins.i15_1.auxiliary import ( + AuxiliaryScan, + combine_auxiliary_requirements, +) @pytest.mark.parametrize( @@ -12,11 +15,13 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, list_of_temperatures=[], ), True, @@ -26,11 +31,29 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, + ), + AuxiliaryScan( + instrument_session="abc", + pin=StandardsPin(capillary="bs1.0", contents="Silicon"), + time_per_pdf=50, + q_max=35, + list_of_temperatures=[], + ), + False, + ), + ( + AuxiliaryScan( + instrument_session="abc", + pin=StandardsPin(capillary="bs1.0", contents="Silicon"), + time_per_pdf=50, + q_max=30, ), AuxiliaryScan( instrument_session="def", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), False, ), @@ -39,11 +62,13 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.5", contents="Silicon"), time_per_pdf=50, + q_max=30, ), False, ), @@ -52,12 +77,14 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, list_of_temperatures=[100, 200, 300], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), False, ), @@ -66,12 +93,14 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, list_of_temperatures=[100, 200, 300], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, list_of_temperatures=[100, 200, 300, 400], ), False, @@ -81,12 +110,14 @@ instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=40, list_of_temperatures=[90, 290, 490], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=25, list_of_temperatures=[95, 250, 300, 450], ), True, @@ -107,18 +138,21 @@ def test_is_suitable_works_as_expected( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=10, + q_max=30, list_of_temperatures=[150, 250, 350], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=25, list_of_temperatures=[100, 200, 300], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, # Max time_per_pdf + q_max=30, # Max q_max list_of_temperatures=[100, 200, 300, 400], # Covers whole temp range ), ), @@ -127,16 +161,19 @@ def test_is_suitable_works_as_expected( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=100, + q_max=40, ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=100, + q_max=40, ), ), ( @@ -144,18 +181,21 @@ def test_is_suitable_works_as_expected( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=40, + q_max=30, list_of_temperatures=[50, 610], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=100, + q_max=30, list_of_temperatures=[100, 200, 300], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=100, + q_max=30, list_of_temperatures=[50, 150, 250, 350, 450, 550, 650], ), ), @@ -177,12 +217,14 @@ def test_attempt_to_combine_with_works_as_expected( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=10, + q_max=30, list_of_temperatures=[150, 250, 350], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Ga/In"), time_per_pdf=50, + q_max=30, list_of_temperatures=[100, 200, 300], ), ), @@ -191,12 +233,14 @@ def test_attempt_to_combine_with_works_as_expected( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=10, + q_max=30, list_of_temperatures=[150, 250, 350], ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, list_of_temperatures=[], ), ), @@ -205,11 +249,13 @@ def test_attempt_to_combine_with_works_as_expected( instrument_session="def", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=10, + q_max=30, ), AuxiliaryScan( instrument_session="abc", pin=StandardsPin(capillary="bs1.0", contents="Silicon"), time_per_pdf=50, + q_max=30, ), ), ], @@ -219,3 +265,177 @@ def test_attempt_to_combine_returns_none_when_auxiliaries_cannot_be_combined( required: AuxiliaryScan, ): assert auxiliary.attempt_to_combine_with(required) is None + + +@pytest.mark.parametrize( + "auxiliaries_1, auxiliaries_2, expected_combined_auxiliaries", + [ + ( + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=25, + list_of_temperatures=None, + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=30, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=40, + list_of_temperatures=None, + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=40, + list_of_temperatures=None, + ), + ], + ), + ( + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=40, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=[100, 150], + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=30, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=25, + list_of_temperatures=None, + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=40, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=[100, 150], + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=25, + list_of_temperatures=None, + ), + ], + ), + ( + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=[100, 200], + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=[400, 500], + ), + ], + [ + AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=None, + ), + AuxiliaryScan( + instrument_session="cm12345-1", + pin=StandardsPin(capillary="fq1.0", contents=None), + time_per_pdf=20.0, + q_max=35, + list_of_temperatures=[100, 200, 300, 400, 500], + ), + ], + ), + ], +) +def test_combine_auxiliary_requirements_combines_auxiliaries_as_expected( + auxiliaries_1: list[AuxiliaryScan], + auxiliaries_2: list[AuxiliaryScan], + expected_combined_auxiliaries: list[AuxiliaryScan], +): + + assert ( + combine_auxiliary_requirements(auxiliaries_1, auxiliaries_2) + == expected_combined_auxiliaries + ) diff --git a/tests/unit_tests/plugins/i15_1/test_i15_1_converter.py b/tests/unit_tests/plugins/i15_1/test_i15_1_converter.py index 934eb708..3efb1b0f 100644 --- a/tests/unit_tests/plugins/i15_1/test_i15_1_converter.py +++ b/tests/unit_tests/plugins/i15_1/test_i15_1_converter.py @@ -6,7 +6,7 @@ import pytest from blueapi.service.model import TaskRequest -from blueapi.worker.event import TaskError +from blueapi.worker.event import TaskError, TaskResult from daq_config_server.models.i15_1.queue_parameters import QueueParameters from daq_config_server.models.i15_1.standards_puck import ( STANDARD_CAPILLARY, @@ -17,6 +17,7 @@ from daq_queuing_service.broadcaster import Broadcaster, serialise from daq_queuing_service.plugins.i15_1.auxiliary import ( + Q_MAX_OPTIONS, AuxiliaryScan, AuxiliaryScanType, TiledAuxiliary, @@ -49,7 +50,7 @@ def i15_1_tasks(): sample=Sample( name="sample_0_2", id="0", - data={"capillary": "fq1.0"}, + data={"capillary": "fq1.0", "composition": "chemical"}, container=Container( id="", positionInParent=ContainerPosition(position=2) ), @@ -63,10 +64,11 @@ def i15_1_tasks(): "list_of_temperatures": [0, 20], "settle_time": 5, "ramp_rate": 10, + "q_max": 30, }, ), ), - id="c5786b54-fd88-4088-8952-8d087cb47028", + id="0", blueapi_calls=[], user=None, ), @@ -77,7 +79,7 @@ def i15_1_tasks(): sample=Sample( name="sample_1_2", id="1", - data={"capillary": "fq1.0"}, + data={"capillary": "fq1.0", "composition": "chemical"}, container=Container( id="", positionInParent=ContainerPosition(position=2) ), @@ -91,10 +93,11 @@ def i15_1_tasks(): "list_of_temperatures": [100, 120], "settle_time": 5, "ramp_rate": 10, + "q_max": 25, }, ), ), - id="7a23c7f6-eecf-4376-a5f3-40b0f221635b", + id="1", blueapi_calls=[], user=None, ), @@ -105,7 +108,7 @@ def i15_1_tasks(): sample=Sample( name="sample_2_2", id="2", - data={"capillary": "fq1.0"}, + data={"capillary": "fq1.0", "composition": "chemical"}, container=Container( id="", positionInParent=ContainerPosition(position=2) ), @@ -119,10 +122,11 @@ def i15_1_tasks(): "list_of_temperatures": [200, 220], "settle_time": 5, "ramp_rate": 10, + "q_max": 35, }, ), ), - id="c96a85c3-cc4b-41e4-b0b5-7292956b6f7c", + id="2", blueapi_calls=[], user=None, ), @@ -133,17 +137,22 @@ def i15_1_tasks(): sample=Sample( name="sample_3_2", id="3", - data={"capillary": "fq1.0"}, + data={"capillary": "fq1.0", "composition": "chemical"}, container=Container( id="", positionInParent=ContainerPosition(position=2) ), positionInContainer=ContainerPosition(position=2), ), experiment_definition=ExperimentDefinition( - name="", id="", data={"time_per_pdf": 20} + name="", + id="", + data={ + "time_per_pdf": 20, + "q_max": 40, + }, ), ), - id="5e2571d9-2f87-45b2-ac3b-b486e83a92d6", + id="3", blueapi_calls=[], user=None, ), @@ -154,7 +163,7 @@ def i15_1_tasks(): sample=Sample( name="sample_4_2", id="4", - data={"capillary": "fq1.0"}, + data={"capillary": "fq1.0", "composition": "chemical"}, container=Container( id="", positionInParent=ContainerPosition(position=2) ), @@ -168,10 +177,11 @@ def i15_1_tasks(): "list_of_temperatures": [400, 420], "settle_time": 5, "ramp_rate": 10, + "q_max": 25, }, ), ), - id="098d3c5a-5bcb-43a3-acc6-c7e2ae050587", + id="4", blueapi_calls=[], user=None, ), @@ -180,11 +190,14 @@ def i15_1_tasks(): return tasks -def make_background_task(capillary: STANDARD_CAPILLARY, time_per_pdf: int) -> Task: +def make_background_task( + capillary: STANDARD_CAPILLARY, time_per_pdf: int, q_max: Q_MAX_OPTIONS +) -> Task: background = AuxiliaryScan( instrument_session="cm12345-1", pin=StandardsPin(capillary=capillary, contents=None), time_per_pdf=time_per_pdf, + q_max=q_max, ) return Task( experiment=I151Converter()._construct_auxiliary_experiment( @@ -215,7 +228,10 @@ def create_task_dict(task_id: str, experiment: dict[str, Any]) -> dict[str, Any] def air_task( - task_id: str, instrument_session: str, time_per_pdf: float + task_id: str, + instrument_session: str, + time_per_pdf: float, + q_max: Q_MAX_OPTIONS, ) -> dict[str, Any]: return create_task_dict( task_id, @@ -226,7 +242,11 @@ def air_task( "experiment_definition": { "name": "Air", "id": "", - "data": {"time_per_pdf": time_per_pdf}, + "data": { + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "required_auxiliary_scans": [], + }, }, }, ) @@ -236,11 +256,25 @@ def empty_capillary_task( task_id: str, instrument_session: str, time_per_pdf: float, + q_max: Q_MAX_OPTIONS, list_of_temperatures: list[float] | None, capillary: STANDARD_CAPILLARY, standards_puck: StandardsPuck, ) -> dict[str, Any]: - data: dict[str, Any] = {"time_per_pdf": time_per_pdf} + data: dict[str, Any] = { + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "required_auxiliary_scans": [ + { + "instrument_session": instrument_session, + "pin": None, + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "list_of_temperatures": None, + "kind": AuxiliaryScanType.AIR, + }, + ], + } if list_of_temperatures is not None: data.update( { @@ -279,14 +313,50 @@ def empty_capillary_task( ) +def air_auxiliary_scan( + instrument_session: str, time_per_pdf: float, q_max: Q_MAX_OPTIONS +) -> dict[str, Any]: + return { + "instrument_session": instrument_session, + "pin": None, + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "list_of_temperatures": None, + "kind": AuxiliaryScanType.AIR, + } + + +def empty_capillary_auxiliary_scan( + instrument_session: str, + time_per_pdf: float, + q_max: Q_MAX_OPTIONS, + list_of_temperatures: list[float] | None, + capillary: STANDARD_CAPILLARY, +) -> dict[str, Any]: + return { + "instrument_session": instrument_session, + "pin": { + "capillary": capillary, + "contents": None, + }, + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "list_of_temperatures": list_of_temperatures, + "kind": AuxiliaryScanType.EMPTY_CAPILLARY, + } + + def standard_sample_task( task_id: str, instrument_session: str, time_per_pdf: float, + q_max: Q_MAX_OPTIONS, capillary: STANDARD_CAPILLARY, contents: STANDARD_SAMPLE, standards_puck: StandardsPuck, + required_empty_capillaries: list[dict[str, Any]], ) -> dict[str, Any]: + return create_task_dict( task_id, { @@ -311,7 +381,14 @@ def standard_sample_task( "experiment_definition": { "name": "Standard Sample", "id": "", - "data": {"time_per_pdf": time_per_pdf}, + "data": { + "time_per_pdf": time_per_pdf, + "q_max": q_max, + "required_auxiliary_scans": [ + air_auxiliary_scan(instrument_session, time_per_pdf, q_max) + ] + + required_empty_capillaries, + }, }, }, ) @@ -341,6 +418,7 @@ def background_found_in_tiled(): filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=25, ) ), ) as mock_get_suitable_tiled_background: @@ -351,7 +429,7 @@ def background_found_in_tiled(): def background_not_found_in_tiled(): with patch( "daq_queuing_service.plugins.i15_1.i15_1_converter.get_suitable_tiled_scan", - MagicMock(return_value=None), + return_value=None, ) as mock_get_suitable_tiled_background: yield mock_get_suitable_tiled_background @@ -367,7 +445,7 @@ def test_given_sample_name_in_correct_format_then_correct_sample_loaded(): experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name=" ", id="", data={"time_per_pdf": 100} + name=" ", id="", data={"time_per_pdf": 100, "q_max": 30} ), sample=make_sample("test_8_1", ""), instrument_session="cm12345-1", @@ -384,9 +462,9 @@ def test_centre_sample_at_room_temperature_uses_correct_params_from_queue_params experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name=" ", id="", data={"time_per_pdf": 100} + name=" ", id="", data={"time_per_pdf": 100, "q_max": 30} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) centring_params = queue_params.room_temperature_centring_params @@ -399,9 +477,9 @@ def test_centre_sample_at_room_temperature_uses_correct_params_from_queue_params "exposure_time": centring_params.exposure_time_per_frame_s, "metadata": { "experiment_definition": ExperimentDefinition( - name=" ", id="", data={"time_per_pdf": 100} + name=" ", id="", data={"time_per_pdf": 100, "q_max": 30} ), - "sample": make_sample("test_8_1", ""), + "sample": make_sample("test_8_1", "", composition="chemical"), }, } @@ -419,9 +497,10 @@ def test_centre_sample_with_blower_uses_correct_params_from_queue_params( "time_per_pdf": 100, "settle_time": 5, "ramp_rate": 10, + "q_max": 30, }, ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = I151Converter()._construct_blueapi_tasks_from_experiment(experiment, "id") @@ -440,9 +519,10 @@ def test_centre_sample_with_blower_uses_correct_params_from_queue_params( "time_per_pdf": 100, "settle_time": 5, "ramp_rate": 10, + "q_max": 30, }, ), - "sample": make_sample("test_8_1", ""), + "sample": make_sample("test_8_1", "", composition="chemical"), }, } @@ -451,9 +531,9 @@ def test_session_and_number_of_tasks_per_experiment_is_expected(): experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name=" ", id="", data={"time_per_pdf": 100} + name=" ", id="", data={"time_per_pdf": 100, "q_max": 30} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = I151Converter()._construct_blueapi_tasks_from_experiment(experiment, "id") @@ -466,9 +546,9 @@ def test_experiment_with_correct_experiment_type_are_converted(): experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name="run_full_collection", id="", data={"time_per_pdf": 100} + name="run_full_collection", id="", data={"time_per_pdf": 100, "q_max": 30} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) task = TaskWithPosition( @@ -490,9 +570,9 @@ def test_experiment_with_no_temperatures_runs_a_room_temperature_collection(): experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name="", id="", data={"time_per_pdf": 100} + name="", id="", data={"time_per_pdf": 100, "q_max": 30} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = I151Converter()._construct_blueapi_tasks_from_experiment(experiment, "id") @@ -502,9 +582,9 @@ def test_experiment_with_no_temperatures_runs_a_room_temperature_collection(): assert tasks[4].params["scan_type"] == ScanType.DATA_COLLECTION assert tasks[4].params["metadata"] == { "experiment_definition": ExperimentDefinition( - name="", id="", data={"time_per_pdf": 100} + name="", id="", data={"time_per_pdf": 100, "q_max": 30} ), - "sample": make_sample("test_8_1", ""), + "sample": make_sample("test_8_1", "", composition="chemical"), } @@ -517,13 +597,14 @@ def test_experiment_with_temperatures_runs_a_blower_collection(): "time_per_pdf": 100, "settle_time": 5, "ramp_rate": 10, + "q_max": 30, }, ) experiment = Experiment( name="test_experiment", experiment_definition=experiment_definition, - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = I151Converter()._construct_blueapi_tasks_from_experiment(experiment, "id") @@ -536,7 +617,7 @@ def test_experiment_with_temperatures_runs_a_blower_collection(): assert tasks[4].params["scan_type"] == ScanType.DATA_COLLECTION assert tasks[4].params["metadata"] == { "experiment_definition": experiment_definition, - "sample": make_sample("test_8_1", ""), + "sample": make_sample("test_8_1", "", composition="chemical"), } @@ -544,7 +625,7 @@ def test_experiment_with_no_sample_converts_to_air_scan(): experiment_definition = ExperimentDefinition( name="", id="", - data={"time_per_pdf": 100}, + data={"time_per_pdf": 100, "q_max": 30}, ) experiment = Experiment( @@ -560,11 +641,12 @@ def test_experiment_with_no_sample_converts_to_air_scan(): assert tasks[1].params == { "full_collection_time": 100, "exposure_time_per_frame": 1.0, + "q_max": 30, "scan_type": ScanType.DATA_COLLECTION, "metadata": { "sample": None, "experiment_definition": ExperimentDefinition( - name="", id="", data={"time_per_pdf": 100} + name="", id="", data={"time_per_pdf": 100, "q_max": 30} ), }, } @@ -579,13 +661,14 @@ def test_wait_for_beam_plans_added_by_converter(): "time_per_pdf": 100, "settle_time": 5, "ramp_rate": 10, + "q_max": 30, }, ) experiment = Experiment( name="test_experiment", experiment_definition=experiment_definition, - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = I151Converter()._construct_blueapi_tasks_from_experiment(experiment, "id") @@ -604,24 +687,25 @@ def test_tiled_backgrounds_added_to_metadata_if_present(): filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=30, ) } experiment_definition = ExperimentDefinition( name="", id="", - data={"time_per_pdf": 100}, + data={"time_per_pdf": 100, "q_max": 30}, ) experiment = Experiment( name="test_experiment", experiment_definition=experiment_definition, - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = converter._construct_blueapi_tasks_from_experiment(experiment, "id") assert tasks[4].params["metadata"] == { "experiment_definition": experiment_definition, - "sample": make_sample("test_8_1", ""), + "sample": make_sample("test_8_1", "", composition="chemical"), "auxiliary_scans": { AuxiliaryScanType.EMPTY_CAPILLARY: TiledAuxiliary( instrument_session="cm12345-1", @@ -631,6 +715,7 @@ def test_tiled_backgrounds_added_to_metadata_if_present(): filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=30, ), }, } @@ -640,9 +725,9 @@ def test_mix_of_experiments_with_correct_experiment_type_are_converted(): good_experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name="run_full_collection", id="", data={"time_per_pdf": 100} + name="run_full_collection", id="", data={"time_per_pdf": 100, "q_max": 30} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) good_task = TaskWithPosition( @@ -677,9 +762,9 @@ def test_if_no_background_found_in_tiled_then_background_scan_added_to_tasks( experiment = Experiment( name="test_experiment", experiment_definition=ExperimentDefinition( - name="run_full_collection", id="", data={"time_per_pdf": 100} + name="run_full_collection", id="", data={"time_per_pdf": 100, "q_max": 40} ), - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) task = Task( @@ -688,14 +773,22 @@ def test_if_no_background_found_in_tiled_then_background_scan_added_to_tasks( ) tasks = converter.pre_process(None, [task], [], []) assert len(tasks) == 4 - assert tasks[0].model_dump() == air_task(tasks[0].id, "cm12345-1", 100.0) + assert tasks[0].model_dump() == air_task(tasks[0].id, "cm12345-1", 100.0, 40) assert tasks[1].model_dump() == empty_capillary_task( - tasks[1].id, "cm12345-1", 100.0, None, "fq1.0", standards_puck + tasks[1].id, "cm12345-1", 100.0, 40, None, "fq1.0", standards_puck ) assert tasks[2].model_dump() == standard_sample_task( - tasks[2].id, "cm12345-1", 100.0, "fq1.0", "Silicon", standards_puck + tasks[2].id, + "cm12345-1", + 100.0, + 40, + "fq1.0", + "Silicon", + standards_puck, + [empty_capillary_auxiliary_scan("cm12345-1", 100.0, 40, None, "fq1.0")], ) - assert converter._tiled_auxiliary_scans == {"1": {}} + + assert list(converter._tiled_auxiliary_scans.values()) == [{}, {}, {}, {}] def test_add_required_background_scans_does_not_add_the_same_background_twice( @@ -703,16 +796,23 @@ def test_add_required_background_scans_does_not_add_the_same_background_twice( i15_1_tasks: list[Task], background_not_found_in_tiled: MagicMock, ): - bg_1 = AuxiliaryScan(instrument_session="cm12345-1", pin=None, time_per_pdf=5) + bg_1 = AuxiliaryScan( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=5, + q_max=30, + ) bg_2 = AuxiliaryScan( instrument_session="cm12345-1", pin=StandardsPin(capillary="bs1.0", contents=None), time_per_pdf=10, + q_max=25, ) bg_3 = AuxiliaryScan( instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=15, + q_max=35, ) def fake_get_required_auxiliary_scans(self: I151Converter, experiment: Experiment): @@ -781,19 +881,19 @@ def test_add_required_background_scans_combines_similar_background_requirements( assert ( isinstance(new_tasks[2].experiment, Experiment) and new_tasks[2].experiment.sample - and new_tasks[2].experiment.sample.name == "Silicon fq1.0" + and new_tasks[2].experiment.sample.name == "Empty fq1.0" ) assert ( - isinstance(new_tasks[6].experiment, Experiment) - and new_tasks[6].experiment.sample - and new_tasks[6].experiment.sample.name == "Empty fq1.0" + isinstance(new_tasks[3].experiment, Experiment) + and new_tasks[3].experiment.sample + and new_tasks[3].experiment.sample.name == "Silicon fq1.0" ) # Should have the maximum time_per_pdf of i15_1_tasks assert new_tasks[0].experiment.experiment_definition.data["time_per_pdf"] == 25 assert new_tasks[1].experiment.experiment_definition.data["time_per_pdf"] == 25 - assert new_tasks[2].experiment.experiment_definition.data["time_per_pdf"] == 25 + assert new_tasks[2].experiment.experiment_definition.data["time_per_pdf"] == 20 + assert new_tasks[3].experiment.experiment_definition.data["time_per_pdf"] == 25 # This one isn't a temperature experiment so needs a separate empty capillary - assert new_tasks[6].experiment.experiment_definition.data["time_per_pdf"] == 20 def test_same_experiment_in_different_instrument_sessions_will_add_backgrounds_in_each( @@ -811,38 +911,75 @@ def test_same_experiment_in_different_instrument_sessions_will_add_backgrounds_i assert len(new_tasks) == 15 - assert new_tasks[0].model_dump() == air_task(new_tasks[0].id, "cm12345-1", 25.0) + assert new_tasks[0].model_dump() == air_task(new_tasks[0].id, "cm12345-1", 25.0, 40) assert new_tasks[1].model_dump() == empty_capillary_task( new_tasks[1].id, "cm12345-1", 25.0, + 30, [0, 100, 200, 300, 400, 500], "fq1.0", standards_puck, ) - assert new_tasks[2].model_dump() == standard_sample_task( - new_tasks[2].id, "cm12345-1", 25.0, "fq1.0", "Silicon", standards_puck + assert new_tasks[2].model_dump() == empty_capillary_task( + new_tasks[2].id, "cm12345-1", 20.0, 40, None, "fq1.0", standards_puck + ) + assert new_tasks[3].model_dump() == standard_sample_task( + new_tasks[3].id, + "cm12345-1", + 25.0, + 40, + "fq1.0", + "Silicon", + standards_puck, + [ + empty_capillary_auxiliary_scan( + "cm12345-1", 25.0, 30, [0, 100, 200, 300, 400, 500], "fq1.0" + ), + empty_capillary_auxiliary_scan("cm12345-1", 20.0, 40, None, "fq1.0"), + ], ) - assert new_tasks[4].model_dump() == air_task(new_tasks[4].id, "different", 10.0) - assert new_tasks[5].model_dump() == empty_capillary_task( - new_tasks[5].id, "different", 10.0, [100, 200], "fq1.0", standards_puck + assert new_tasks[5].model_dump() == air_task(new_tasks[5].id, "different", 10.0, 25) + assert new_tasks[6].model_dump() == empty_capillary_task( + new_tasks[6].id, "different", 10.0, 25, [100, 200], "fq1.0", standards_puck ) - assert new_tasks[6].model_dump() == standard_sample_task( - new_tasks[6].id, "different", 10.0, "fq1.0", "Silicon", standards_puck + assert new_tasks[7].model_dump() == standard_sample_task( + new_tasks[7].id, + "different", + 10.0, + 25, + "fq1.0", + "Silicon", + standards_puck, + [empty_capillary_auxiliary_scan("different", 10.0, 25, [100, 200], "fq1.0")], ) - assert new_tasks[8].model_dump() == air_task( - new_tasks[8].id, "also_different", 10.0 - ) - assert new_tasks[9].model_dump() == empty_capillary_task( - new_tasks[9].id, "also_different", 10.0, [200, 300], "fq1.0", standards_puck + assert new_tasks[9].model_dump() == air_task( + new_tasks[9].id, "also_different", 10.0, 35 ) - assert new_tasks[10].model_dump() == standard_sample_task( - new_tasks[10].id, "also_different", 10.0, "fq1.0", "Silicon", standards_puck + assert new_tasks[10].model_dump() == empty_capillary_task( + new_tasks[10].id, + "also_different", + 10.0, + 35, + [200, 300], + "fq1.0", + standards_puck, ) - assert new_tasks[12].model_dump() == empty_capillary_task( - new_tasks[12].id, "cm12345-1", 20.0, None, "fq1.0", standards_puck + assert new_tasks[11].model_dump() == standard_sample_task( + new_tasks[11].id, + "also_different", + 10.0, + 35, + "fq1.0", + "Silicon", + standards_puck, + [ + empty_capillary_auxiliary_scan( + "also_different", 10.0, 35, [200, 300], "fq1.0" + ) + ], ) @@ -868,12 +1005,72 @@ def test_add_required_background_scans_if_found_in_tiled_then_no_background_adde filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=25, ) } for task in i15_1_tasks } +async def test_auxiliary_scans_have_their_own_required_tiled_auxiliaries_added_to_data( + queue_with_i15_1_plugin: TaskQueue, + queue_params: QueueParameters, + background_not_found_in_tiled: MagicMock, +): + queue_params.do_wait_for_beam = False + queue = await queue_with_i15_1_plugin.get_queue() + assert [task.experiment.experiment_definition.name for task in queue[:4]] == [ # type: ignore + # Auxiliaries added to queue + ScanType.AIR, + ScanType.EMPTY_CAPILLARY, + ScanType.EMPTY_CAPILLARY, + ScanType.STANDARD_SAMPLE, + ] + + air_scan = await queue_with_i15_1_plugin.get_next_call_once_available() + air_scan.put_in_progress() + + air_tiled_scan = TiledAuxiliary( + instrument_session="cm12345-1", + tiled_id="fake_tiled_id", + pin=None, + time_per_pdf=25, + filepath=Path(""), + instrument_session_directory=Path(""), + filename="air_scan.nxs", + q_max=40, + ) + background_not_found_in_tiled.return_value = air_tiled_scan + + await queue_with_i15_1_plugin.complete_call( + air_scan, TaskResult(outcome="success", result=None, type="None") + ) + async with queue_with_i15_1_plugin._modifying: + queue_with_i15_1_plugin._sync() + + queue = await queue_with_i15_1_plugin.get_queue() + + assert [task.experiment.experiment_definition.name for task in queue[:3]] == [ # type: ignore + ScanType.EMPTY_CAPILLARY, + ScanType.EMPTY_CAPILLARY, + ScanType.STANDARD_SAMPLE, + ] + converter = queue_with_i15_1_plugin._converter + assert isinstance(converter, I151Converter) + for task in queue: + assert ( + converter._tiled_auxiliary_scans[task.id][AuxiliaryScanType.AIR] + == air_tiled_scan + ) + assert any( + "metadata" in call.task_request.params + and "auxiliary_scans" in call.task_request.params["metadata"] + and call.task_request.params["metadata"]["auxiliary_scans"] + == {AuxiliaryScanType.AIR: air_tiled_scan.model_dump()} + for call in task.blueapi_calls + ) + + async def test_queue_with_i15_1_converter_can_sync(queue_with_i15_1_plugin: TaskQueue): for i, task in enumerate(queue_with_i15_1_plugin._tasks.values()): task.experiment.instrument_session = str(i) @@ -891,8 +1088,9 @@ def test__ensure_background_in_queue_or_tiled_returns_if_suitable_already_queued instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=25, + q_max=30, ) - new_tasks = [make_background_task("fq1.0", 25)] + new_tasks = [make_background_task("fq1.0", 25, 30)] result = i15_1_converter._ensure_auxiliary_in_queue_or_tiled( background, None, new_tasks, "task_id", "cm12345-1" ) @@ -906,11 +1104,12 @@ def test__ensure_background_in_queue_or_tiled_returns_if_current_task_is_suitabl instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=25, + q_max=40, ) new_tasks: list[Task] = [] result = i15_1_converter._ensure_auxiliary_in_queue_or_tiled( background, - TaskWithPosition.from_task(make_background_task("fq1.0", 25)), + TaskWithPosition.from_task(make_background_task("fq1.0", 25, q_max=40)), new_tasks, "task_id", "cm12345-1", @@ -925,17 +1124,18 @@ def test__ensure_background_in_queue_or_tiled_modifies_queued_background_if_poss instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=25, + q_max=40, ) - current_task = TaskWithPosition.from_task(make_background_task("fq1.0", 25)) + current_task = TaskWithPosition.from_task(make_background_task("fq1.0", 25, 40)) # Current task is suitable but not a background assert isinstance(current_task.experiment, Experiment) current_task.experiment.experiment_definition.name = "Not a background" - new_tasks = [make_background_task("fq1.0", 10)] + new_tasks = [make_background_task("fq1.0", 10, 30)] result = i15_1_converter._ensure_auxiliary_in_queue_or_tiled( background, current_task, new_tasks, "task_id", "cm12345-1" ) assert len(result) == 1 - assert_tasks_equal(result[0], make_background_task("fq1.0", 25)) + assert_tasks_equal(result[0], make_background_task("fq1.0", 25, 40)) def test__ensure_background_in_queue_or_tiled_adds_background_if_none_suitable_in_queue( @@ -946,12 +1146,13 @@ def test__ensure_background_in_queue_or_tiled_adds_background_if_none_suitable_i instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=25, + q_max=35, ) result = i15_1_converter._ensure_auxiliary_in_queue_or_tiled( background, None, [], "task_id", "cm12345-1" ) assert len(result) == 1 - assert_tasks_equal(result[0], make_background_task("fq1.0", 25)) + assert_tasks_equal(result[0], make_background_task("fq1.0", 25, 35)) def test__ensure_background_in_queue_or_tiled_saves_tiled_info_if_exists( @@ -963,6 +1164,7 @@ def test__ensure_background_in_queue_or_tiled_saves_tiled_info_if_exists( instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=25, + q_max=35, ) result = i15_1_converter._ensure_auxiliary_in_queue_or_tiled( background, None, [], "task_id", "cm12345-1" @@ -978,6 +1180,7 @@ def test__ensure_background_in_queue_or_tiled_saves_tiled_info_if_exists( filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=25, ) } } @@ -989,16 +1192,12 @@ def test__ensure_background_in_queue_or_tiled_saves_tiled_info_if_exists( (1, ScanType.AIR), # 2 is robot load # 3 is wait_for_beam - # 4 is centring - # 5 is wait_for_beam - (6, ScanType.EMPTY_CAPILLARY), - # 7 is robot unload - # 8 is robot load - # 9 is wait_for_beam - # 10 is centring - # 11 is wait_for_beam - (12, ScanType.STANDARD_SAMPLE), - (18, ScanType.DATA_COLLECTION), + (4, ScanType.EMPTY_CAPILLARY), + # 5 is robot unload + # etc + (8, ScanType.EMPTY_CAPILLARY), + (14, ScanType.STANDARD_SAMPLE), + (20, ScanType.DATA_COLLECTION), ], ) async def test_queued_data_collections_have_correct_scan_type_parameter( @@ -1019,18 +1218,19 @@ def test_i15_1_tasks_can_be_serialised(): filepath=Path(""), instrument_session_directory=Path(""), filename="", + q_max=35, ) } experiment_definition = ExperimentDefinition( name="", id="", - data={"time_per_pdf": 100}, + data={"time_per_pdf": 100, "q_max": 25}, ) experiment = Experiment( name="test_experiment", experiment_definition=experiment_definition, - sample=make_sample("test_8_1", ""), + sample=make_sample("test_8_1", "", composition="chemical"), instrument_session="cm12345-1", ) tasks = converter._construct_blueapi_tasks_from_experiment(experiment, "id") diff --git a/tests/unit_tests/plugins/i15_1/test_tiled_interaction.py b/tests/unit_tests/plugins/i15_1/test_tiled_interaction.py index 1cd01b66..5f71c96c 100644 --- a/tests/unit_tests/plugins/i15_1/test_tiled_interaction.py +++ b/tests/unit_tests/plugins/i15_1/test_tiled_interaction.py @@ -10,6 +10,7 @@ from daq_queuing_service.plugins.i15_1.auxiliary import ( AuxiliaryScan, + AuxiliaryScanType, TiledAuxiliary, ) from daq_queuing_service.plugins.i15_1.tiled_interaction import ( @@ -28,13 +29,13 @@ def mock_current_time(): @pytest.fixture() def mock_tiled_searches( tiled_client: MagicMock, -) -> tuple[MagicMock, MagicMock, MagicMock, MagicMock, MagicMock, MagicMock]: +) -> tuple[MagicMock, MagicMock, MagicMock, MagicMock, MagicMock]: result_1 = MagicMock() result_1.metadata = { "start": { "time": 1, - "experiment_definition": {"data": {"time_per_pdf": 10}}, + "experiment_definition": {"data": {"time_per_pdf": 25, "q_max": 35}}, "sample_info": None, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10000", @@ -45,33 +46,69 @@ def mock_tiled_searches( result_2.metadata = { "start": { "time": 10, - "experiment_definition": {"data": {"time_per_pdf": 11}}, + "experiment_definition": {"data": {"time_per_pdf": 11, "q_max": 35}}, "sample_info": {"data": {"capillary": "fq1.0"}}, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10001", "scan_type": "Empty Capillary", + "auxiliary_scans": { + AuxiliaryScanType.AIR.value: { + "instrument_session": "cm12345-1", + "pin": None, + "time_per_pdf": 25.0, + "list_of_temperatures": None, + "tiled_id": "tiled_id_1", + "filename": "i15-1-10000.nxs", + "instrument_session_directory": "/path/to/data/2026/cm12345-1", + "filepath": "/path/to/data/2026/cm12345-1/i15-1-10000.nxs", + } + }, } } result_3 = MagicMock() result_3.metadata = { "start": { "time": 2, - "experiment_definition": {"data": {"time_per_pdf": 9}}, + "experiment_definition": {"data": {"time_per_pdf": 9, "q_max": 30}}, "sample_info": {"data": {"capillary": "fq1.0"}}, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10002", "scan_type": "Empty Capillary", + "auxiliary_scans": { + AuxiliaryScanType.AIR.value: { + "instrument_session": "cm12345-1", + "pin": None, + "time_per_pdf": 25.0, + "list_of_temperatures": None, + "tiled_id": "tiled_id_1", + "filename": "i15-1-10000.nxs", + "instrument_session_directory": "/path/to/data/2026/cm12345-1", + "filepath": "/path/to/data/2026/cm12345-1/i15-1-10000.nxs", + } + }, } } result_4 = MagicMock() result_4.metadata = { "start": { "time": 5, - "experiment_definition": {"data": {"time_per_pdf": 12}}, + "experiment_definition": {"data": {"time_per_pdf": 12, "q_max": 25}}, "sample_info": {"data": {"capillary": "fq1.0"}}, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10003", "scan_type": "Empty Capillary", + "auxiliary_scans": { + AuxiliaryScanType.AIR.value: { + "instrument_session": "cm12345-1", + "pin": None, + "time_per_pdf": 25.0, + "list_of_temperatures": None, + "tiled_id": "tiled_id_1", + "filename": "i15-1-10000.nxs", + "instrument_session_directory": "/path/to/data/2026/cm12345-1", + "filepath": "/path/to/data/2026/cm12345-1/i15-1-10000.nxs", + } + }, } } result_5 = MagicMock() @@ -84,17 +121,30 @@ def mock_tiled_searches( "list_of_temperatures": [100, 200, 300], "ramp_rate": 0, "settle_time": 10, + "q_max": 35, } }, "sample_info": {"data": {"capillary": "fq1.0"}}, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10004", "scan_type": "Empty Capillary", + "auxiliary_scans": { + AuxiliaryScanType.AIR.value: { + "instrument_session": "cm12345-1", + "pin": None, + "time_per_pdf": 25.0, + "list_of_temperatures": None, + "tiled_id": "tiled_id_1", + "filename": "i15-1-10000.nxs", + "instrument_session_directory": "/path/to/data/2026/cm12345-1", + "filepath": "/path/to/data/2026/cm12345-1/i15-1-10000.nxs", + } + }, } } - search_result_6 = MagicMock() - search_result_6.search = MagicMock( + search_result_5 = MagicMock() + search_result_5.search = MagicMock( return_value={ "tiled_id_1": result_1, "tiled_id_2": result_2, @@ -104,8 +154,6 @@ def mock_tiled_searches( } ) - search_result_5 = MagicMock() - search_result_5.search = MagicMock(return_value=search_result_6) search_result_4 = MagicMock() search_result_4.search = MagicMock(return_value=search_result_5) search_result_3 = MagicMock() @@ -113,7 +161,9 @@ def mock_tiled_searches( search_result_2 = MagicMock() search_result_2.search = MagicMock(return_value=search_result_3) - tiled_client.search = MagicMock(return_value=search_result_2) + tiled_client["i15-1/cm12345/cm12345-1"].search = MagicMock( + return_value=search_result_2 + ) return ( tiled_client, @@ -121,36 +171,36 @@ def mock_tiled_searches( search_result_3, search_result_4, search_result_5, - search_result_6, ) def test_get_suitable_tiled_background_makes_expected_searches( - mock_tiled_searches: tuple[ - MagicMock, MagicMock, MagicMock, MagicMock, MagicMock, MagicMock - ], + mock_tiled_searches: tuple[MagicMock, MagicMock, MagicMock, MagicMock, MagicMock], queue_params: QueueParameters, ): - client, search_2, search_3, search_4, search_5, search_6 = mock_tiled_searches + client, search_2, search_3, search_4, search_5 = mock_tiled_searches get_suitable_tiled_scan( client, - AuxiliaryScan(instrument_session="cm12345-1", pin=None, time_per_pdf=10), + AuxiliaryScan( + instrument_session="cm12345-1", pin=None, time_per_pdf=10, q_max=35 + ), queue_params.tiled_stale_time_s, ) - client.search.assert_called_once_with(Eq(key="start.instrument", value="i15-1")) - search_2.search.assert_called_once_with( - Eq(key="start.instrument_session", value="cm12345-1") + client["i15-1/cm12345/cm12345-1"].search.assert_called_once_with( # type: ignore + Eq("stop.exit_status", "success") ) - search_3.search.assert_called_once_with(Eq("stop.exit_status", "success")) - search_4.search.assert_called_once_with( + search_2.search.assert_called_once_with( Comparison("ge", "stop.time", 30 - queue_params.tiled_stale_time_s) ) - search_5.search.assert_called_once_with( + search_3.search.assert_called_once_with( In(key="start.scan_type", value=["Air", "Empty Capillary", "Standard Sample"]) ) - search_6.search.assert_called_once_with( + search_4.search.assert_called_once_with( KeyPresent("start.experiment_definition.data.time_per_pdf") ) + search_5.search.assert_called_once_with( + KeyPresent("start.experiment_definition.data.q_max") + ) @pytest.mark.parametrize( @@ -161,15 +211,33 @@ def test_get_suitable_tiled_background_makes_expected_searches( instrument_session="cm12345-1", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=10, + q_max=30, ), TiledAuxiliary( instrument_session="cm12345-1", tiled_id="tiled_id_2", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=11, + q_max=35, filename="i15-1-10001.nxs", instrument_session_directory=Path("/path/to/data/2026/cm12345-1"), filepath=Path("/path/to/data/2026/cm12345-1/i15-1-10001.nxs"), + requires=[ + TiledAuxiliary( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=25.0, + q_max=35, + list_of_temperatures=None, + requires=[], + tiled_id="tiled_id_1", + filename="i15-1-10000.nxs", + filepath=Path("/path/to/data/2026/cm12345-1/i15-1-10000.nxs"), + instrument_session_directory=Path( + "/path/to/data/2026/cm12345-1" + ), + ) + ], ), ), ( @@ -178,16 +246,34 @@ def test_get_suitable_tiled_background_makes_expected_searches( pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=10, list_of_temperatures=[150, 250], + q_max=30, ), TiledAuxiliary( instrument_session="cm12345-1", tiled_id="tiled_id_5", pin=StandardsPin(capillary="fq1.0", contents=None), time_per_pdf=15, + q_max=35, filename="i15-1-10004.nxs", instrument_session_directory=Path("/path/to/data/2026/cm12345-1"), filepath=Path("/path/to/data/2026/cm12345-1/i15-1-10004.nxs"), list_of_temperatures=[100, 200, 300], + requires=[ + TiledAuxiliary( + instrument_session="cm12345-1", + pin=None, + time_per_pdf=25.0, + list_of_temperatures=None, + q_max=35, + requires=[], + tiled_id="tiled_id_1", + filename="i15-1-10000.nxs", + filepath=Path("/path/to/data/2026/cm12345-1/i15-1-10000.nxs"), + instrument_session_directory=Path( + "/path/to/data/2026/cm12345-1" + ), + ) + ], ), ), ], @@ -210,7 +296,9 @@ def test_get_suitable_tiled_background_returns_none_if_no_matching_backgrounds_f assert ( get_suitable_tiled_scan( client, - AuxiliaryScan(instrument_session="cm12345-1", pin=None, time_per_pdf=10), + AuxiliaryScan( + instrument_session="cm12345-1", pin=None, time_per_pdf=10, q_max=30 + ), 1000000, ) is None @@ -236,7 +324,9 @@ def test_get_suitable_tiled_background_skips_tiled_scan_if_no_capillary_found_an with caplog.at_level(logging.WARNING): tiled_scan = get_suitable_tiled_scan( client, - AuxiliaryScan(instrument_session="cm12345-1", pin=None, time_per_pdf=10), + AuxiliaryScan( + instrument_session="cm12345-1", pin=None, time_per_pdf=10, q_max=30 + ), 1000000, ) assert tiled_scan is None @@ -251,7 +341,7 @@ def test_get_suitable_tiled_background_skips_tiled_scan_if_scan_type_doesnt_matc result.metadata = { "start": { "time": 10, - "experiment_definition": {"data": {"time_per_pdf": 11}}, + "experiment_definition": {"data": {"time_per_pdf": 11, "q_max": 25}}, "sample_info": {"data": {"capillary": "fq1.0"}}, "data_session_directory": "/path/to/data/2026/cm12345-1", "scan_file": "i15-1-10001", @@ -262,7 +352,9 @@ def test_get_suitable_tiled_background_skips_tiled_scan_if_scan_type_doesnt_matc with caplog.at_level(logging.WARNING): tiled_scan = get_suitable_tiled_scan( client, - AuxiliaryScan(instrument_session="cm12345-1", pin=None, time_per_pdf=10), + AuxiliaryScan( + instrument_session="cm12345-1", pin=None, time_per_pdf=10, q_max=30 + ), 1000000, ) assert tiled_scan is None diff --git a/tests/unit_tests/test_api.py b/tests/unit_tests/test_api.py index 1dd51775..de777037 100644 --- a/tests/unit_tests/test_api.py +++ b/tests/unit_tests/test_api.py @@ -158,7 +158,7 @@ def test_get_queued_tasks_returns_queued_task(test_client: TestClient): "sample": { "name": "test_8_2", "id": "2", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -198,7 +198,7 @@ def test_get_queued_tasks_returns_queued_task(test_client: TestClient): "sample": { "name": "test_8_3", "id": "3", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -238,7 +238,7 @@ def test_get_queued_tasks_returns_queued_task(test_client: TestClient): "sample": { "name": "test_8_4", "id": "4", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -285,7 +285,7 @@ def test_get_queued_tasks_can_filter_by_task_status(test_client: TestClient): "sample": { "name": "test_8_2", "id": "2", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -342,7 +342,7 @@ async def test_get_all_tasks_can_filter_by_task_status(test_client: TestClient): "sample": { "name": "test_8_1", "id": "1", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -693,7 +693,7 @@ async def test_cancel_tasks_removes_task_from_queue_and_returns_tasks( "sample": { "name": "test_8_3", "id": "3", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -733,7 +733,7 @@ async def test_cancel_tasks_removes_task_from_queue_and_returns_tasks( "sample": { "name": "test_8_4", "id": "4", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": {"id": "", "positionInParent": {"position": 2}}, "positionInContainer": {"position": 2}, }, @@ -845,7 +845,7 @@ async def test_cancel_all_tasks_removes_all_queued_tasks_from_queue_and_returns_ "sample": { "name": "test_8_3", "id": "3", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": { "id": "", "positionInParent": { @@ -892,7 +892,7 @@ async def test_cancel_all_tasks_removes_all_queued_tasks_from_queue_and_returns_ "sample": { "name": "test_8_4", "id": "4", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": { "id": "", "positionInParent": { @@ -945,7 +945,7 @@ def test_get_task_by_position_returns_expected_task(test_client: TestClient): "sample": { "name": "test_8_3", "id": "3", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": { "id": "", "positionInParent": { @@ -997,7 +997,7 @@ def test_get_task_by_id_returns_expected_task(test_client: TestClient): "sample": { "name": "test_8_3", "id": "3", - "data": {"capillary": "fq1.0"}, + "data": {"capillary": "fq1.0", "composition": "chemical"}, "container": { "id": "", "positionInParent": {