Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f77dfc7
added formula editor and began to thread the additinal_observables th…
YektaY Sep 17, 2024
a364407
changed ci.yml so tests run on my repo
YektaY Sep 17, 2024
c662919
getting test working on my fork
YektaY Sep 17, 2024
4105351
replaced ci.yml with another version
YektaY Sep 17, 2024
33e6d5d
changing file name
YektaY Sep 17, 2024
ddaf51e
added missing file
YektaY Sep 17, 2024
03e5c89
added requirements.txt
YektaY Sep 17, 2024
ea989f2
small fix
YektaY Sep 17, 2024
dae7281
added pydantic as a rec
YektaY Sep 17, 2024
7fb87ce
added orjson as a rec
YektaY Sep 17, 2024
5303577
added torch as a rec
YektaY Sep 17, 2024
3554907
added xopt as a rec
YektaY Sep 17, 2024
6fb6c69
changed recs
YektaY Sep 17, 2024
816198b
added windows rec.txt
YektaY Sep 17, 2024
e19a75d
commented out turbo test
YektaY Sep 17, 2024
48938a6
small change
YektaY Sep 17, 2024
85ca985
testing alt of home test
YektaY Sep 17, 2024
f1bc9a8
removeing fork from tests
YektaY Sep 18, 2024
d57e34b
merge in master
YektaY Sep 18, 2024
38d4043
small rename of file and change to xopt install
YektaY Sep 18, 2024
2c8df2d
fixes to test_core_subprocess.py
YektaY Sep 19, 2024
577e8ee
changing python version to test with in the github actions
YektaY Sep 19, 2024
876a51a
changing xopt install to conda insead of pip
YektaY Sep 19, 2024
979b15b
testing an issue with windows test
YektaY Sep 19, 2024
4501ecb
testing fix
YektaY Sep 19, 2024
5f9a63b
fix to one of the two test failing on windows
YektaY Sep 19, 2024
e69f7b8
fix to two tests
YektaY Sep 20, 2024
7b52925
swapped fork for spawn
YektaY Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/run-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# This workflow will install badger dependencies, lint with flake8, and run the test suite, for all combinations
# of operating systems and version numbers specified in the matrix

name: Build Status

on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]

permissions:
contents: read

jobs:
build:
if: ${{ github.repository == 'YektaY/Badger' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: [3.11, 3.12]
env:
DISPLAY: ':99.0'
QT_MAC_WANTS_LAYER: 1 # PyQT gui tests involving qtbot interaction on macOS will fail without this

steps:
- uses: actions/checkout@v3
- name: Setup conda
uses: conda-incubator/setup-miniconda@v2
with:
python-version: ${{ matrix.python-version }}
miniforge-variant: Mambaforge
miniforge-version: latest
activate-environment: badger-env
environment-file: environment-dev.yml
- name: Install xopt v2.1+
shell: bash -el {0}
run: |
mamba install -c conda-forge xopt
- name: Install python packages
shell: bash -el {0}
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
mamba install flake8 zipp
mamba install --file requirements.txt --file windows-dev-requirements.txt
else
mamba install flake8 zipp
mamba install --file requirements.txt
fi
- name: Install pyqt5
shell: bash -el {0}
run: |
pip install PyQt5
- name: Install packages for testing a pyqt app on linux
shell: bash -el {0}
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt install xvfb herbstluftwm libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
sudo /sbin/start-stop-daemon --start --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset
sleep 3
sudo /sbin/start-stop-daemon --start --pidfile /tmp/custom_herbstluftwm_99.pid --make-pidfile --background --exec /usr/bin/herbstluftwm
sleep 1
fi
- name: Install Badger
shell: bash -el {0}
run: |
pip install .
- name: Test with pytest
shell: bash -el {0}
run: |
python scripts/run_tests.py
5 changes: 5 additions & 0 deletions environment-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: badger-env
channels:
- conda-forge
- defaults
- pytorch
12 changes: 12 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pandas
pyyaml
coolname
pyqtgraph
qdarkstyle
pillow
requests
tqdm
pytest
pytest-qt
pytest-mock
xopt
207 changes: 207 additions & 0 deletions src/badger/gui/default/components/archive_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import logging
from typing import (List, Optional)
from PyQt5.QtGui import QDrag, QKeyEvent
from PyQt5.QtCore import (QAbstractTableModel, QMimeData, QModelIndex, QObject,
Qt, QUrl, QVariant, pyqtSignal)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import (QAbstractItemView, QHBoxLayout, QHeaderView, QLabel,
QLineEdit, QPushButton, QTableView, QVBoxLayout, QWidget)

logger = logging.getLogger(__name__)


class ArchiveResultsTableModel(QAbstractTableModel):
"""This table model holds the results of an archiver appliance PV search. This search is for names matching
the input search words, and the results are a list of PV names that match that search.

Parameters
----------
parent : QObject, optional
The parent item of this table
"""

def __init__(self, parent: QObject = None) -> None:
super().__init__(parent=parent)
self.results_list = []
self.column_names = ("PV",)

def rowCount(self, parent: QObject) -> int:
"""Return the row count of the table"""
if parent is not None and parent.isValid():
return 0
return len(self.results_list)

def columnCount(self, parent: QObject) -> int:
"""Return the column count of the table"""
if parent is not None and parent.isValid():
return 0
return len(self.column_names)

def data(self, index: QModelIndex, role: int) -> QVariant:
"""Return the data for the associated role. Currently only supporting DisplayRole."""
if not index.isValid():
return QVariant()

if role != Qt.DisplayRole:
return QVariant()

return self.results_list[index.row()]

def headerData(self, section, orientation, role=Qt.DisplayRole) -> QVariant:
"""Return data associated with the header"""
if role != Qt.DisplayRole:
return super().headerData(section, orientation, role)

return str(self.column_names[section])

def flags(self, index: QModelIndex) -> Qt.ItemFlags:
"""Return flags that determine how users can interact with the items in the table"""
if index.isValid():
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled

def append(self, pv: str) -> None:
"""Appends a row to this table given the PV name as input"""
self.beginInsertRows(QModelIndex(), len(self.results_list), len(self.results_list))
self.results_list.append(pv)
self.endInsertRows()
self.layoutChanged.emit()

def replace_rows(self, pvs: List[str]) -> None:
"""Overwrites any existing rows in the table with the input list of PV names"""
self.beginInsertRows(QModelIndex(), 0, len(pvs) - 1)
self.results_list = pvs
self.endInsertRows()
self.layoutChanged.emit()

def clear(self) -> None:
"""Clear out all data stored in this table"""
self.beginRemoveRows(QModelIndex(), 0, len(self.results_list))
self.results_list = []
self.endRemoveRows()
self.layoutChanged.emit()

def sort(self, col: int, order=Qt.AscendingOrder) -> None:
"""Sort the table by PV name"""
self.results_list.sort(reverse=order == Qt.DescendingOrder)
self.layoutChanged.emit()


class ArchiveSearchWidget(QWidget):
"""
The ArchiveSearchWidget is a display widget for showing the results of a PV search using an instance of the
EPICS archiver appliance. Currently the only type of search supported is for PV names matching an input search
string, though this can be extended in the future.

Parameters
----------
parent : QObject, optional
The parent item of this widget
"""
append_PVs_requested = pyqtSignal(str)
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent=parent)

self.network_manager = QNetworkAccessManager()
self.network_manager.finished.connect(self.populate_results_list)

self.resize(400, 800)
self.layout = QVBoxLayout()

self.archive_title_label = QLabel("Archive URL:")
self.archive_url_textedit = QLineEdit("lcls-archapp.slac.stanford.edu")
self.archive_url_textedit.setFixedWidth(250)
self.archive_url_textedit.setFixedHeight(25)

self.search_label = QLabel("Pattern:")
self.search_box = QLineEdit()
self.search_button = QPushButton("Search")
self.search_button.setDefault(True)
self.search_button.clicked.connect(self.request_archiver_info)

self.loading_label = QLabel("Loading...")
self.loading_label.hide()

self.results_table_model = ArchiveResultsTableModel()
self.results_view = QTableView(self)
self.results_view.setModel(self.results_table_model)
self.results_view.setProperty("showDropIndicator", False)
self.results_view.setDragDropOverwriteMode(False)
self.results_view.setDragEnabled(True)
self.results_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.results_view.setSelectionBehavior(QAbstractItemView.SelectRows)
self.results_view.setDropIndicatorShown(True)
self.results_view.setCornerButtonEnabled(False)
self.results_view.setSortingEnabled(True)
self.results_view.verticalHeader().setVisible(False)
self.results_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.results_view.startDrag = self.startDragAction

self.archive_url_layout = QHBoxLayout()
self.archive_url_layout.addWidget(self.archive_title_label)
self.archive_url_layout.addWidget(self.archive_url_textedit)
self.layout.addLayout(self.archive_url_layout)
self.search_layout = QHBoxLayout()
self.search_layout.addWidget(self.search_label)
self.search_layout.addWidget(self.search_box)
self.search_layout.addWidget(self.search_button)
self.layout.addLayout(self.search_layout)
self.layout.addWidget(self.loading_label)
self.layout.addWidget(self.results_view)
self.insert_button = QPushButton("Add PVs")
self.insert_button.clicked.connect(lambda:self.append_PVs_requested.emit(self.selectedPVs()))
self.results_view.doubleClicked.connect(lambda:self.append_PVs_requested.emit(self.selectedPVs()))
self.layout.addWidget(self.insert_button)
self.setLayout(self.layout)

def selectedPVs(self) -> str:
"""Figure out based on which indexes were selected, the list of PVs (by string name)
The user was hoping to insert into the table. Concatenate them into string form i.e.
<pv1>, <pv2>, <pv3>"""
indices = self.results_view.selectedIndexes()
pv_list = ""
for index in indices:
pv_name = self.results_table_model.results_list[index.row()]
pv_list += pv_name + ", "
return pv_list[:-2]

def startDragAction(self, supported_actions) -> None:
"""
The method to be called when a user initiates a drag action for one of the results in the table. The current
reason for this functionality is the ability to drag a PV name onto a plot to automatically start drawing
data for that PV
"""
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setText(self.selectedPVs())
drag.setMimeData(mime_data)
drag.exec_()

def keyPressEvent(self, e: QKeyEvent) -> None:
"""Special key press tracker, just so that if enter or return is pressed the formula dialog attempts to submit the formula"""
if e.key() == Qt.Key_Return or e.key() == Qt.Key_Enter:
self.request_archiver_info()
return super().keyPressEvent(e)

def request_archiver_info(self) -> None:
"""Send the search request to the archiver appliance based on the search string typed into the text box"""
search_text = self.search_box.text()
search_text = search_text.replace("?", ".")
url_string = (
f"http://{self.archive_url_textedit.text()}/"
f"retrieval/bpl/searchForPVsRegex?regex=.*{search_text}.*"
)
request = QNetworkRequest(QUrl(url_string))
self.network_manager.get(request)
self.loading_label.show()

def populate_results_list(self, reply: QNetworkReply) -> None:
"""Slot called when the archiver appliance returns search results. Will populate the table with the results"""
self.loading_label.hide()
if reply.error() == QNetworkReply.NoError:
self.results_table_model.clear()
bytes_str = reply.readAll()
pv_list = str(bytes_str, "utf-8").split()
self.results_table_model.replace_rows(pv_list)
else:
logger.error(f"Could not retrieve archiver results due to: {reply.error()}")
reply.deleteLater()
19 changes: 18 additions & 1 deletion src/badger/gui/default/components/env_cbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from PyQt5.QtWidgets import QComboBox, QCheckBox, QStyledItemDelegate, QLabel, QListWidget, QFrame
from PyQt5.QtCore import QRegExp

from badger.gui.default.components.traces_table import FormulaDialog
from badger.gui.default.components.archive_search import ArchiveSearchWidget

from .collapsible_box import CollapsibleBox
from .var_table import VariableTable
from .obj_table import ObjectiveTable
Expand Down Expand Up @@ -233,12 +236,18 @@ def init_ui(self):
self.edit_obj = edit_obj = QLineEdit()
edit_obj.setPlaceholderText('Filter objectives...')
edit_obj.setFixedWidth(192)
extra_obs = QPushButton("Add Observable")
extra_obs.clicked.connect(self.formulaMenu)
self.check_only_obj = check_only_obj = QCheckBox('Show Checked Only')
check_only_obj.setChecked(False)
hbox_action_obj.addWidget(edit_obj)
hbox_action_obj.addWidget(extra_obs)
hbox_action_obj.addStretch()
hbox_action_obj.addWidget(check_only_obj)




self.obj_table = ObjectiveTable()
vbox_obj_edit.addWidget(self.obj_table)
hbox_obj.addWidget(edit_obj_col)
Expand Down Expand Up @@ -294,7 +303,6 @@ def init_ui(self):
vbox_lbl_sta.addWidget(lbl_sta)
vbox_lbl_sta.addStretch(1)
hbox_sta.addWidget(lbl_sta_col)

edit_sta_col = QWidget()
vbox_sta_edit = QVBoxLayout(edit_sta_col)
vbox_sta_edit.setContentsMargins(0, 0, 0, 0)
Expand All @@ -318,6 +326,15 @@ def init_ui(self):

self.setContentLayout(vbox)

def formulaMenu(self):
self.formula= FormulaDialog(self)
self.formula.show()

def archiveSearchMenu(self):
self.archive_search = ArchiveSearchWidget()
self.archive_search.show()


def config_logic(self):
self.dict_con = {}

Expand Down
2 changes: 1 addition & 1 deletion src/badger/gui/default/components/obj_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs):
header.setSectionResizeMode(1, QHeaderView.Stretch)
self.setColumnWidth(0, 20)
self.setColumnWidth(2, 192)

self.addtl_obs = []
self.all_objectives = []
self.objectives = []
self.selected = {} # track obj selected status
Expand Down
7 changes: 6 additions & 1 deletion src/badger/gui/default/components/routine_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ def refresh_ui(self, routine: Routine = None, silent: bool = False):
self.env_box.check_only_obj.setChecked(True)
self.env_box.edit_obj.clear()
self.env_box.obj_table.set_selected(objectives)
#self.env_box.obj_table.addtl_obs(routine.additional_observables)
self.env_box.obj_table.set_rules(routine.vocs.objectives)

constraints = routine.vocs.constraints
Expand Down Expand Up @@ -939,7 +940,8 @@ def _compose_routine(self) -> Routine:
relative_to_current=relative_to_current,
vrange_limit_options=vrange_limit_options,
initial_point_actions=initial_point_actions,
additional_variables=self.env_box.var_table.addtl_vars
additional_variables=self.env_box.var_table.addtl_vars,
additional_observables=self.env_box.obj_table.addtl_obs
)

# Check if any user warnings were caught
Expand Down Expand Up @@ -1027,3 +1029,6 @@ def delete(self):
remove_routine(name)

return 0



Loading