diff --git a/src/conftest.py b/src/conftest.py index afa39a1c..acd7ab51 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -379,6 +379,7 @@ def filters(sim_registry): mapping = { "255id_testing": MapAdapter(bluesky_mapping), + "255bm_testing": MapAdapter(bluesky_mapping), } tree = MapAdapter(mapping) @@ -389,13 +390,12 @@ def tiled_client(): app = build_app(tree) with Context.from_app(app) as context: client = from_context(context) - yield client["255id_testing"] + yield client @pytest.fixture() def catalog(tiled_client): - cat = Catalog(client=tiled_client) - # cat = mock.AsyncMock() + cat = Catalog(client=tiled_client["255id_testing"]) return cat diff --git a/src/firefly/controller.py b/src/firefly/controller.py index 24167193..ccfd12ce 100644 --- a/src/firefly/controller.py +++ b/src/firefly/controller.py @@ -333,11 +333,15 @@ async def finalize_new_window(self, action): # Send the current devices to the window await action.window.update_devices(self.registry) - def finalize_run_browser_window(self, action): - """Connect up signals that are specific to the run browser window.""" + @asyncSlot(QAction) + async def finalize_run_browser_window(self, action): + """Connect up run browser signals and load initial data.""" display = action.display self.run_updated.connect(display.update_running_scan) self.run_stopped.connect(display.update_running_scan) + # Set initial state for the run_browser + config = load_config()['database']['tiled'] + await display.change_catalog(config['entry_node']) def finalize_status_window(self, action): """Connect up signals that are specific to the voltmeters window.""" @@ -652,12 +656,6 @@ async def add_queue_item(self, item): if getattr(self, "_queue_client", None) is not None: await self._queue_client.add_queue_item(item) - @QtCore.Slot() - def show_sample_viewer_window(self): - return self.show_window( - FireflyMainWindow, ui_dir / "sample_viewer.ui", name="sample_viewer" - ) - @QtCore.Slot(bool) def set_open_environment_action_state(self, is_open: bool): """Update the readback value for opening the queueserver environment.""" diff --git a/src/firefly/run_browser/client.py b/src/firefly/run_browser/client.py index 86e6744b..c556ce40 100644 --- a/src/firefly/run_browser/client.py +++ b/src/firefly/run_browser/client.py @@ -1,3 +1,4 @@ +import asyncio import datetime as dt import logging import warnings @@ -7,6 +8,7 @@ import numpy as np import pandas as pd from tiled import queries +from qasync import asyncSlot from haven import exceptions from haven.catalog import Catalog @@ -16,13 +18,31 @@ class DatabaseWorker: selected_runs: Sequence = [] + catalog: Catalog = None - def __init__(self, catalog=None, *args, **kwargs): - if catalog is None: - catalog = Catalog() - self.catalog = catalog + def __init__(self, tiled_client, *args, **kwargs): + self.client = tiled_client super().__init__(*args, **kwargs) + @asyncSlot(str) + async def change_catalog(self, catalog_name: str): + """Change the catalog being used for pulling data. + + *catalog_name* should be an entry in *worker.tiled_client()*. + """ + def get_catalog(name): + return Catalog(self.client[catalog_name]) + + loop = asyncio.get_running_loop() + self.catalog = await loop.run_in_executor(None, get_catalog, catalog_name) + + async def catalog_names(self): + def get_names(): + return list(self.client.keys()) + + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, get_names) + async def filtered_nodes(self, filters: Mapping): case_sensitive = False log.debug(f"Filtering nodes: {filters}") @@ -49,6 +69,7 @@ async def filtered_nodes(self, filters: Mapping): return runs async def load_distinct_fields(self): + """Get distinct metadata fields for filterable metadata.""" new_fields = {} target_fields = [ diff --git a/src/firefly/run_browser/display.py b/src/firefly/run_browser/display.py index d814ca57..302c0246 100644 --- a/src/firefly/run_browser/display.py +++ b/src/firefly/run_browser/display.py @@ -13,6 +13,7 @@ from qtpy.QtGui import QStandardItem, QStandardItemModel from ophyd_async.core import Device from ophyd import Device as ThreadedDevice +from tiled.client.container import Container from pydm import PyDMChannel from firefly import display @@ -60,16 +61,29 @@ class RunBrowserDisplay(display.FireflyDisplay): # Counter for keeping track of UI hints for long DB hits _busy_hinters: Counter - def __init__(self, root_node=None, args=None, macros=None, **kwargs): + def __init__(self, args=None, macros=None, **kwargs): super().__init__(args=args, macros=macros, **kwargs) self.selected_runs = [] self._running_db_tasks = {} self._busy_hinters = Counter() - self.db = DatabaseWorker(catalog=root_node) - # Load the list of all runs for the selection widget - self.db_task(self.load_runs(), name="init_load_runs") - # Load the list of filters' field values into the comboboxes - self.db_task(self.update_combobox_items(), name="update_combobox_items") + + async def setup_database(self, tiled_client: Container, catalog_name: str): + """Prepare to use a set of databases accessible through *tiled_client*. + + Parameters + ========== + Each key in *tiled_client* should be """ + self.db = DatabaseWorker(tiled_client) + self.ui.catalog_combobox.addItems(await self.db.catalog_names()) + await self.change_catalog(catalog_name) + + async def change_catalog(self, catalog_name: str): + """Activate a different catalog in the Tiled server.""" + await self.db_task(self.db.change_catalog(catalog_name), name="change_catalog") + await self.db_task(asyncio.gather( + self.load_runs(), + self.update_combobox_items() + ), name="change_catalog") def db_task(self, coro, name="default task"): """Executes a co-routine as a database task. Existing database @@ -143,7 +157,6 @@ async def update_combobox_items(self): """""" with self.busy_hints(run_table=False, run_widgets=False, filter_widgets=True): fields = await self.db.load_distinct_fields() - print(fields) for field_name, cb in [ ("plan_name", self.ui.filter_plan_combobox), ("sample_name", self.ui.filter_sample_combobox), @@ -247,7 +260,8 @@ async def update_devices(self, registry): def setup_bss_channels(self, bss: Device | ThreadedDevice): """Setup channels to update the proposal and ESAF ID boxes.""" - self.proposal_channel.disconnect() + if getattr(self, "proposal_channel", None) is not None: + self.proposal_channel.disconnect() self.proposal_channel = PyDMChannel( address=f"haven://{bss.proposal.proposal_id.name}", value_slot=partial( @@ -256,7 +270,8 @@ def setup_bss_channels(self, bss: Device | ThreadedDevice): checkbox=self.ui.filter_current_proposal_checkbox, ) ) - self.esaf_channel.disconnect() + if getattr(self, "esaf_channel", None) is not None: + self.esaf_channel.disconnect() self.esaf_channel = PyDMChannel( address=f"haven://{bss.esaf.esaf_id.name}", value_slot=partial( diff --git a/src/firefly/run_browser/run_browser.ui b/src/firefly/run_browser/run_browser.ui index 363bbe61..cd5e6def 100644 --- a/src/firefly/run_browser/run_browser.ui +++ b/src/firefly/run_browser/run_browser.ui @@ -36,7 +36,7 @@ - false + true Catalog: @@ -44,9 +44,9 @@ - + - false + true @@ -223,7 +223,7 @@ 0 0 373 - 560 + 531 diff --git a/src/firefly/run_browser/tests/test_client.py b/src/firefly/run_browser/tests/test_client.py index 209b4008..f043cdbe 100644 --- a/src/firefly/run_browser/tests/test_client.py +++ b/src/firefly/run_browser/tests/test_client.py @@ -3,23 +3,35 @@ from firefly.run_browser.client import DatabaseWorker +@pytest.fixture() +async def worker(tiled_client): + worker = DatabaseWorker(tiled_client) + await worker.change_catalog("255id_testing") + return worker + + +@pytest.mark.asyncio +async def test_catalog_names(worker): + assert (await worker.catalog_names()) == ["255id_testing", "255bm_testing"] + + @pytest.mark.asyncio -async def test_filter_runs(catalog): - worker = DatabaseWorker(catalog=catalog) +async def test_filter_runs(worker): runs = await worker.load_all_runs(filters={"plan": "xafs_scan"}) # Check that the runs were filtered assert len(runs) == 1 @pytest.mark.asyncio -async def test_distinct_fields(catalog): - worker = DatabaseWorker(catalog=catalog) +async def test_distinct_fields(worker): distinct_fields = await worker.load_distinct_fields() # Check that the dictionary has the right structure for key in ["sample_name"]: assert key in distinct_fields.keys() + + # ----------------------------------------------------------------------------- # :author: Mark Wolfman # :email: wolfman@anl.gov diff --git a/src/firefly/run_browser/tests/test_display.py b/src/firefly/run_browser/tests/test_display.py index 4d439ea0..8dbdba29 100644 --- a/src/firefly/run_browser/tests/test_display.py +++ b/src/firefly/run_browser/tests/test_display.py @@ -21,7 +21,7 @@ def bss(sim_registry): @pytest.fixture() -async def display(qtbot, catalog, mocker): +async def display(qtbot, tiled_client, catalog, mocker): mocker.patch( "firefly.run_browser.widgets.ExportDialog.exec_", return_value=QFileDialog.Accepted, @@ -31,20 +31,16 @@ async def display(qtbot, catalog, mocker): return_value=["/net/s255data/export/test_file.nx"], ) mocker.patch("firefly.run_browser.client.DatabaseWorker.export_runs") - display = RunBrowserDisplay(root_node=catalog) + display = RunBrowserDisplay() qtbot.addWidget(display) display.clear_filters() # Wait for the initial database load to process - await display._running_db_tasks["init_load_runs"] - await display._running_db_tasks["update_combobox_items"] + await display.setup_database(tiled_client, catalog_name="255id_testing") # Set up some fake data run = [run async for run in catalog.values()][0] display.db.selected_runs = [run] await display.update_1d_signals() run_data = await run.data() - expected_xdata = run_data.energy_energy - expected_ydata = np.log(run_data.I0_net_counts / run_data.It_net_counts) - expected_ydata = np.gradient(expected_ydata, expected_xdata) # Set the controls to describe the data we want to test x_combobox = display.ui.signal_x_combobox x_combobox.addItem("energy_energy") @@ -58,7 +54,6 @@ async def display(qtbot, catalog, mocker): return display -@pytest.mark.asyncio async def test_db_task(display): async def test_coro(): return 15 @@ -67,7 +62,6 @@ async def test_coro(): assert result == 15 -@pytest.mark.asyncio async def test_db_task_interruption(display): async def test_coro(sleep_time): await asyncio.sleep(sleep_time) @@ -90,7 +84,6 @@ def test_load_runs(display): assert display.ui.runs_total_label.text() == str(display.runs_model.rowCount()) -@pytest.mark.asyncio async def test_update_selected_runs(display): # Change the proposal item item = display.runs_model.item(0, 1) @@ -102,7 +95,6 @@ async def test_update_selected_runs(display): assert len(display.db.selected_runs) > 0 -@pytest.mark.asyncio async def test_update_selected_runs(display): # Change the proposal item item = display.runs_model.item(0, 1) @@ -120,7 +112,6 @@ async def test_clear_plots(display): assert display.plot_1d_view.clear_runs.called -@pytest.mark.asyncio async def test_metadata(display): # Change the proposal item display.ui.run_tableview.selectRow(0) @@ -130,7 +121,6 @@ async def test_metadata(display): assert "xafs_scan" in text -@pytest.mark.asyncio async def test_1d_plot_signals(catalog, display): # Check that the 1D plot was created plot_widget = display.ui.plot_1d_view @@ -152,8 +142,7 @@ async def test_1d_plot_signals(catalog, display): # Warns: Task was destroyed but it is pending! -@pytest.mark.asyncio -async def test_1d_plot_signal_memory(catalog, display): +async def test_1d_plot_signal_memory(display): """Do we remember the signals that were previously selected.""" # Check that the 1D plot was created plot_widget = display.ui.plot_1d_view @@ -174,7 +163,6 @@ async def test_1d_plot_signal_memory(catalog, display): # Warns: Task was destroyed but it is pending! -@pytest.mark.asyncio async def test_1d_hinted_signals(catalog, display): display.ui.plot_1d_hints_checkbox.setChecked(True) # Check that the 1D plot was created @@ -196,7 +184,6 @@ async def test_1d_hinted_signals(catalog, display): ), f"unhinted signal found in {combobox.objectName()}." -@pytest.mark.asyncio async def test_update_1d_plot(catalog, display): display.plot_1d_view.plot_runs = MagicMock() display.plot_1d_view.autoRange = MagicMock() @@ -233,7 +220,6 @@ async def test_update_running_scan(display): # Warns: Task was destroyed but it is pending! -@pytest.mark.asyncio async def test_2d_plot_signals(catalog, display): # Check that the 1D plot was created plot_widget = display.ui.plot_2d_view @@ -248,7 +234,6 @@ async def test_2d_plot_signals(catalog, display): assert combobox.findText("It_net_counts") > -1 -@pytest.mark.asyncio async def test_update_2d_plot(catalog, display): display.plot_2d_item.setRect = MagicMock() # Load test data @@ -279,7 +264,6 @@ async def test_update_2d_plot(catalog, display): display.plot_2d_item.setRect.assert_called_with(-100, -80, 200, 160) -@pytest.mark.asyncio async def test_update_multi_plot(catalog, display): run = await catalog["7d1daf1d-60c7-4aa7-a668-d1cd97e5335f"] expected_xdata = await run["energy_energy"] @@ -353,7 +337,6 @@ def test_busy_hints_multiple(display): assert display.ui.detail_tabwidget.isEnabled() -@pytest.mark.asyncio async def test_update_combobox_items(display): """Check that the comboboxes get the distinct filter fields.""" assert display.ui.filter_plan_combobox.count() > 0 @@ -366,7 +349,6 @@ async def test_update_combobox_items(display): assert display.ui.filter_beamline_combobox.count() > 0 -@pytest.mark.asyncio async def test_export_button_enabled(catalog, display): assert not display.export_button.isEnabled() # Update the list with 1 run and see if the control gets enabled @@ -380,7 +362,6 @@ async def test_export_button_enabled(catalog, display): assert not display.export_button.isEnabled() -@pytest.mark.asyncio async def test_export_button_clicked(catalog, display, mocker, qtbot): # Set up a run to be tested against run = MagicMock() @@ -454,6 +435,12 @@ def test_update_bss_filters(display): checkbox.setChecked(False) update_slot("99531") assert combobox.currentText() == "89321" + + +def test_catalog_choices(display, tiled_client): + combobox = display.ui.catalog_combobox + items = [combobox.itemText(idx) for idx in range(combobox.count())] + assert items == ["255id_testing", "255bm_testing"] # ----------------------------------------------------------------------------- # :author: Mark Wolfman diff --git a/src/haven/catalog.py b/src/haven/catalog.py index 7ec4185a..453d05e3 100644 --- a/src/haven/catalog.py +++ b/src/haven/catalog.py @@ -309,6 +309,11 @@ class Catalog: are structured, so can make some assumptions and takes care of boiler-plate code (e.g. reshaping maps, etc). + Parameters + ========== + client + A Tiled client that has scan UIDs as its keys. + """ _client = None