diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..07e6c76 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,48 @@ + +name: Integration + +on: + pull_request + +permissions: + contents: read + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + pip install flake8 pytest Pyqt6 pytest-qt + sudo apt-get update + sudo apt-get install -y libegl1 xvfb + if [ -f slitmaskgui/requirements.txt ]; then pip install -r slitmaskgui/requirements.txt; fi + + - name: Lint with flake8 + run: | + source .venv/bin/activate + flake8 slitmaskgui/ --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv + flake8 slitmaskgui/ --count --exit-zero --max-complexity=10 --statistics #--max-line-length=120 --exclude=.venv + + - name: Test + run: | + source .venv/bin/activate + export QT_QPA_PLATFORM=offscreen + xvfb-run -a python3 -m pytest slitmaskgui/tests/ + #test: diff --git a/.gitignore b/.gitignore index daff69d..b0f375c 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ gaia_starlist.txt slitmaskgui/tests/testfiles/look_at_later.json ======= >>>>>>> c53fca9e73a6906023b2dc175c50be15bb18df13 +center_coord_starlist.txt diff --git a/slitmaskgui/app.py b/slitmaskgui/app.py index c5aa5d4..1693606 100644 --- a/slitmaskgui/app.py +++ b/slitmaskgui/app.py @@ -16,7 +16,6 @@ #just importing everything for now. When on the final stages I will not import what I don't need import sys -import random import logging logging.basicConfig( filename="main.log", @@ -29,27 +28,27 @@ from slitmaskgui.target_list_widget import TargetDisplayWidget from slitmaskgui.mask_gen_widget import MaskGenWidget from slitmaskgui.menu_bar import MenuBar -from slitmaskgui.mask_viewer import interactiveSlitMask, WavelengthView -from slitmaskgui.sky_viewer import SkyImageView +from slitmaskgui.mask_widgets.slitmask_view import interactiveSlitMask +from slitmaskgui.mask_widgets.waveband_view import WavelengthView +from slitmaskgui.mask_widgets.sky_viewer import SkyImageView from slitmaskgui.mask_configurations import MaskConfigurationsWidget from slitmaskgui.slit_position_table import SlitDisplay -from slitmaskgui.mask_view_tab_bar import TabBar -from PyQt6.QtCore import Qt, QSize, pyqtSlot -from PyQt6.QtGui import QFontDatabase +from slitmaskgui.mask_widgets.mask_view_tab_bar import TabBar +from slitmaskgui.configure_mode.mode_toggle_button import ShowControllerButton +from slitmaskgui.configure_mode.mask_controller import MaskControllerWidget +from slitmaskgui.configure_mode.csu_display_widget import CsuDisplauWidget +from slitmaskgui.offline_mode import OfflineMode +from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtWidgets import ( QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, - QLabel, QSizePolicy, QSplitter, - QLayout, - QTreeWidgetItem, - QTreeWidget, QTabWidget, - QComboBox + QStackedLayout ) @@ -58,21 +57,6 @@ main_logger = logging.getLogger() main_logger.info("starting logging") -""" -currently use center of priority doesn't work (don't know the problem will diagnose it at some later point) -need to make it so that it doesn't randomly generate a starlist with random priority -add more logging to all the functions -""" - -""" -Things to do before launch -photo display of the stars -use center of priority should work -ability to modulate slit width -actually use a starlist instead of generating your own -add logging to everything -ability to state max slit length -""" class MainWindow(QMainWindow): def __init__(self): @@ -82,19 +66,31 @@ def __init__(self): self.setMenuBar(MenuBar()) #sets the menu bar self.update_theme() + #----------------------------definitions--------------------------- main_logger.info("app: doing definitions") + + self.connection_status = OfflineMode() + self.start_checking_internet_connection() mask_config_widget = MaskConfigurationsWidget() mask_gen_widget = MaskGenWidget() + self.mode_toggle_button = ShowControllerButton() + mask_controller_widget = MaskControllerWidget() + csu_display_widget = CsuDisplauWidget() self.target_display = TargetDisplayWidget() self.interactive_slit_mask = interactiveSlitMask() self.slit_position_table = SlitDisplay() self.wavelength_view = WavelengthView() self.sky_view = SkyImageView() - self.mask_tab = TabBar(slitmask=self.interactive_slit_mask,waveview=self.wavelength_view,skyview=self.sky_view) - + + #------------- stacked layout in mask_tab -------------------- + self.slitmask_and_csu_display = QStackedLayout() + self.slitmask_and_csu_display.addWidget(self.interactive_slit_mask) + self.slitmask_and_csu_display.addWidget(csu_display_widget) + + self.mask_tab = TabBar(slitmask_layout=self.slitmask_and_csu_display,waveview=self.wavelength_view,skyview=self.sky_view) #---------------------------------connections----------------------------- main_logger.info("app: doing connections") @@ -104,26 +100,33 @@ def __init__(self): self.target_display.selected_le_star.connect(self.interactive_slit_mask.get_row_from_star_name) self.interactive_slit_mask.select_star.connect(self.target_display.select_corresponding) self.wavelength_view.row_selected.connect(self.interactive_slit_mask.select_corresponding_row) - self.mask_tab.waveview_change.connect(self.wavelength_view.re_initialize_scene) + self.interactive_slit_mask.new_slit_positions.connect(self.mask_tab.initialize_spectral_view) mask_gen_widget.change_data.connect(self.target_display.change_data) - mask_gen_widget.change_slit_image.connect(self.interactive_slit_mask.change_slit_and_star) + mask_gen_widget.change_slit_image.connect(self.interactive_slit_mask.update_slit_and_star) mask_gen_widget.change_row_widget.connect(self.slit_position_table.change_data) - mask_gen_widget.send_mask_config.connect(mask_config_widget.update_table) - mask_gen_widget.change_wavelength_data.connect(self.wavelength_view.get_spectra_of_star) + mask_gen_widget.send_mask_config.connect(mask_config_widget.initialize_configuration) mask_config_widget.change_data.connect(self.target_display.change_data) mask_config_widget.change_row_widget.connect(self.slit_position_table.change_data) - mask_config_widget.change_slit_image.connect(self.interactive_slit_mask.change_slit_and_star) + mask_config_widget.change_slit_image.connect(self.interactive_slit_mask.update_slit_and_star) mask_config_widget.reset_scene.connect(self.reset_scene) - mask_config_widget.update_image.connect(self.sky_view.show_image) + mask_config_widget.update_image.connect(self.sky_view.update_image) mask_config_widget.change_name_above_slit_mask.connect(self.interactive_slit_mask.update_name_center_pa) #if the data is changed connections - self.slit_position_table.tell_unsaved.connect(mask_config_widget.update_table) + self.slit_position_table.tell_unsaved.connect(mask_config_widget.update_table_to_unsaved) mask_config_widget.data_to_save_request.connect(self.slit_position_table.data_saved) self.slit_position_table.data_changed.connect(mask_config_widget.save_data_to_mask) + #sending to csu connections + self.mode_toggle_button.connect_controller_with_config(mask_controller_widget,mask_config_widget) + mask_controller_widget.connect_controller_with_slitmask_display(csu_display_widget) + self.mode_toggle_button.button.clicked.connect(self.mode_toggle_button.on_button_clicked) + self.mode_toggle_button.button.clicked.connect(self.switch_modes) + + self.connection_status.current_mode.connect(self.switch_internet_connection_mode) + #-----------------------------------layout----------------------------- main_logger.info("app: setting up layout") @@ -132,6 +135,12 @@ def __init__(self): main_splitter = QSplitter() self.splitterV2 = QSplitter() self.mask_viewer_main = QVBoxLayout() + self.stacked_layout = QStackedLayout() + switcher_widget = QWidget() + + self.stacked_layout.addWidget(mask_gen_widget) + self.stacked_layout.addWidget(mask_controller_widget) + switcher_widget.setLayout(self.stacked_layout) self.interactive_slit_mask.setContentsMargins(0,0,0,0) self.slit_position_table.setContentsMargins(0,0,0,0) @@ -140,28 +149,25 @@ def __init__(self): mask_config_widget.setMinimumSize(1,1) mask_gen_widget.setMinimumSize(1,1) - self.splitterV2.addWidget(mask_config_widget) - self.splitterV2.addWidget(mask_gen_widget) + self.splitterV2.addWidget(switcher_widget) + self.splitterV2.addWidget(self.mode_toggle_button) self.splitterV2.setOrientation(Qt.Orientation.Vertical) self.splitterV2.setContentsMargins(0,0,0,0) - self.layoutH1.addWidget(self.slit_position_table)#temp_widget2) + self.layoutH1.addWidget(self.slit_position_table) self.layoutH1.addWidget(self.mask_tab) - # self.layoutH1.addWidget(self.combobox) self.layoutH1.setSpacing(0) self.layoutH1.setContentsMargins(9,9,9,9) widgetH1 = QWidget() widgetH1.setLayout(self.layoutH1) self.splitterV1.addWidget(widgetH1) - # self.splitterV1.setCollapsible(0,False) self.splitterV1.addWidget(self.target_display) self.splitterV1.setOrientation(Qt.Orientation.Vertical) self.splitterV1.setContentsMargins(0,0,0,0) main_splitter.addWidget(self.splitterV1) - # main_splitter.setCollapsible(0,False) main_splitter.addWidget(self.splitterV2) self.setCentralWidget(main_splitter) @@ -218,6 +224,24 @@ def update_theme(self): else: with open("slitmaskgui/dark_mode.qss", "r") as f: self.setStyleSheet(f.read()) + + def switch_modes(self): + index = abs(self.stacked_layout.currentIndex()-1) + self.stacked_layout.setCurrentIndex(index) + self.slitmask_and_csu_display.setCurrentIndex(index) + button_text = "Configuration Mode (ON)" if index == 1 else "Configuration Mode (OFF)" + self.mode_toggle_button.button.setText(button_text) + + def start_checking_internet_connection(self): + self.connection_status.start_checking_internet_connection() + self.connection_status.start_timer() + + def switch_internet_connection_mode(self): + self.sky_view.offline = self.connection_status.offline + self.setWindowTitle(f"LRIS-2 Slit Configuration Tool ({repr(self.connection_status)})") + + + diff --git a/slitmaskgui/backend/sample.py b/slitmaskgui/backend/sample.py index 1ca3559..3a41347 100644 --- a/slitmaskgui/backend/sample.py +++ b/slitmaskgui/backend/sample.py @@ -2,6 +2,7 @@ from astropy.coordinates import SkyCoord import astropy.units as u import random +import numpy as np def query_gaia_starlist_rect(ra_center, dec_center, width_arcmin=5, height_arcmin=10, n_stars=100, output_file='gaia_starlist.txt'): @@ -24,6 +25,15 @@ def query_gaia_starlist_rect(ra_center, dec_center, width_arcmin=5, height_arcmi sign, dec_d, dec_m, dec_s = coord.dec.signed_dms dec_d = sign * dec_d + # parallax = row['parallax'] # in mas + # app_mag = row['phot_g_mean_mag'] + + # if parallax > 0: + # distance_pc = 1000.0 / parallax + # abs_mag = app_mag - 5 * (np.log10(distance_pc) - 1) + # else: + # abs_mag = float(0) + line = f"{name:<15} {int(ra_h):02d} {int(ra_m):02d} {ra_s:05.2f} {int(dec_d):+03d} {int(dec_m):02d} {abs(dec_s):04.1f} 2000.0 vmag={row['phot_g_mean_mag']:.2f} priority={random.randint(1,2000)}\n" f.write(line) # Output center info @@ -31,15 +41,15 @@ def query_gaia_starlist_rect(ra_center, dec_center, width_arcmin=5, height_arcmi # Example call — replace RA/Dec with your actual center -run = False +run = False if run: - ra = "00 42 44.00" - dec = "+41 16 09.00" + ra = "10 20 10.00" + dec = "-10 04 00.10" query_gaia_starlist_rect( ra_center=ra, # RA in degrees dec_center=dec, # Dec in degrees width_arcmin=5, height_arcmin=10, n_stars=104, - output_file='andromeda_galaxy.txt' + output_file='gaia_starlist.txt' ) \ No newline at end of file diff --git a/slitmaskgui/backend/star_list.py b/slitmaskgui/backend/star_list.py index dc15e2a..5b5e5f7 100644 --- a/slitmaskgui/backend/star_list.py +++ b/slitmaskgui/backend/star_list.py @@ -46,7 +46,7 @@ class StarList: #with auto run you can select if the json is complete or not already #this means that if you have a complete list of all the stars as if it rand thorough this class, then you can select auto run as false #then you can use the send functions without doing a bunch of computation - def __init__(self,payload,ra,dec,slit_width=0,pa=0,auto_run=True,use_center_of_priority=False): + def __init__(self,payload,ra=0,dec=0,slit_width=0,pa=0,auto_run=True,use_center_of_priority=False): self.payload = json.loads(payload) ra_coord,dec_coord = ra, dec if use_center_of_priority: @@ -61,36 +61,26 @@ def __init__(self,payload,ra,dec,slit_width=0,pa=0,auto_run=True,use_center_of_p def calc_mask(self,all_stars): slit_mask = SlitMask(all_stars,center=self.center, slit_width= self.slit_width, pa= self.pa) - return json.loads(slit_mask.return_mask()) def export_mask_config(self,file_path): - # file_path = f'{os.getcwd()}/{mask_name}.json' with open(file_path,'w') as f: json.dump(self.payload,f,indent=4) - # return file_path + def send_mask(self, mask_name="untitled"): return self.payload - def send_target_list(self): return [[x["name"],x["priority"],x["vmag"],x["ra"],x["dec"],x["center distance"]] for x in self.payload] - def send_interactive_slit_list(self): - #have to convert it to dict {bar_num:(position,star_name)} - #imma just act rn like all the stars are in sequential order - #I am going to have an optimize function that actually gets the right amount of stars with good positions - #its going to also order them by bar - total_pixels = 252 - slit_dict = { - i: (obj["x_mm"], obj["bar_id"], obj["name"]) - for i, obj in enumerate(self.payload[:72]) + obj["bar_id"]: (obj["x_mm"], obj["name"]) + for obj in self.payload[:72] if "bar_id" in obj } - return slit_dict + def send_list_for_wavelength(self): old_ra_dec_list = [[x["bar_id"],x["ra"],x["dec"]]for x in self.payload] ra_dec_list =[] @@ -98,13 +88,13 @@ def send_list_for_wavelength(self): return ra_dec_list def send_row_widget_list(self): - #the reason why the bar id is plus 1 is to transl sorted_row_list = sorted( ([obj["bar_id"]+1, obj["x_mm"], obj["slit_width"]] for obj in self.payload[:72] if "bar_id" in obj), key=lambda x: x[0] ) return sorted_row_list + def find_center_of_priority(self): """ ∑ coordinates * priority CoP coordinate = ------------------------ @@ -136,7 +126,7 @@ def generate_skyview(self): key = (hips, width, height, ra, dec, fov) if key in HIPS_CACHE: return HIPS_CACHE[key] - + hdulist = hips2fits.query( hips=hips, width=width, #in pixels diff --git a/slitmaskgui/configure_mode/csu_display_widget.py b/slitmaskgui/configure_mode/csu_display_widget.py new file mode 100644 index 0000000..516d406 --- /dev/null +++ b/slitmaskgui/configure_mode/csu_display_widget.py @@ -0,0 +1,79 @@ +from PyQt6.QtWidgets import ( + QPushButton, QWidget, QVBoxLayout, QDialog, QLabel, + QGraphicsRectItem, QGraphicsScene, QGraphicsView, QGraphicsLayout, + ) +from PyQt6.QtGui import QColor, QPen, QBrush +from PyQt6.QtCore import pyqtSignal, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QPointF +from slitmaskgui.mask_widgets.mask_objects import SimpleBarPair, MoveableFieldOfView, CustomGraphicsView + +PLATE_SCALE = 0.7272 #(mm/arcsecond) on the sky +CSU_HEIGHT = PLATE_SCALE*60*10 #height of csu in mm (height is 10 arcmin) +CSU_WIDTH = PLATE_SCALE*60*5 #width of the csu in mm (widgth is 5 arcmin) +DEMO_WIDTH = 260 +DEMO_HEIGHT = 75 + + +class CsuDisplauWidget(QWidget): + connect_with_controller = pyqtSignal() + def __init__(self): + super().__init__() + """will recieve data as position, bar_id, width""" + + main_widget = QLabel("CSU Display Mode") + + self.default_slit_width = 0.7 # + self.scene = QGraphicsScene(0,0,DEMO_WIDTH,DEMO_HEIGHT) + self.scene.setSceneRect(self.scene.itemsBoundingRect()) + + self.view = CustomGraphicsView(self.scene) + # -------------- set default layout numbers ------------- + default_layout_list = [[0,DEMO_WIDTH/2,x,True] for x in range(12)] + [[0,DEMO_WIDTH/2,x,False] for x in range(12)] + + # -------------- layout ------------------ + main_layout = QVBoxLayout() + main_layout.addWidget(main_widget) + main_layout.addWidget(self.view) + main_layout.setContentsMargins(0,0,0,0) + self.setLayout(main_layout) + + # ------------- initialize layout ------------- + self.set_layout(default_layout_list) + + def set_layout(self,pos_list): + self.scene.clear() + bar_list = [SimpleBarPair(x[0],x[1],x[2],x[3]) for x in pos_list] + [self.scene.addItem(bar) for bar in bar_list] + fov = MoveableFieldOfView(height=DEMO_HEIGHT,width=DEMO_WIDTH,x=0,thickness=2) + self.scene.addItem(fov) # add green field of view + self.scene.setSceneRect(fov.boundingRect()) + self.view = CustomGraphicsView(self.scene) + + def get_slits(self, slits): + bar_list = [SimpleBarPair(s.width,s.x,s.id,left_side=True) for s in slits] + bar_list += [SimpleBarPair(s.width,s.x,s.id,left_side=False) for s in slits] + try: + current_bars = [item for item in self.scene.items() if isinstance(item, SimpleBarPair)] + self.animate_bars(previous_bars=current_bars,future_bars=bar_list) + except: + pass #109.08 is like the middle they are at + + def handle_configuration_mode(self): + self.connect_with_controller.emit() + def update_layout(self,bars): + pass + + def animate_bars(self, previous_bars: list, future_bars: list): + self.anim_group = QParallelAnimationGroup() + for prev, future in zip(previous_bars, future_bars): + + anim = QPropertyAnimation(prev, b"pos_anim") + anim.setDuration(1500) + # anim.setEasingCurve(QEasingCurve.Type.InOutCubic) + anim.setEndValue(QPointF(future.x_pos,future.slit_width)) # animate the change in slitwidth and change in x_pos + + self.anim_group.addAnimation(anim) + self.anim_group.start() + + + + diff --git a/slitmaskgui/configure_mode/csu_worker.py b/slitmaskgui/configure_mode/csu_worker.py new file mode 100644 index 0000000..ff0f302 --- /dev/null +++ b/slitmaskgui/configure_mode/csu_worker.py @@ -0,0 +1,99 @@ +""" +This function will instruct the csu +""" + +from PyQt6.QtCore import QThread, pyqtSignal +from lris2csu.remote import CSURemote +from lris2csu.slit import Slit, MaskConfig +from slitmaskgui.mask_widgets.mask_objects import ErrorWidget +from logging import getLogger + +# Setup logging +logger = getLogger('mktl') + +class CSUWorkerThread(QThread): + # Define signals to send results back to the main thread + reset_signal = pyqtSignal(object) + calibrate_signal = pyqtSignal(object) # Calibration response + status_signal = pyqtSignal(list) # List of slits + stop_signal = pyqtSignal(object) + slit_config_updated_signal = pyqtSignal(object) + + def __init__(self, c: CSURemote): + super().__init__() + self.c = c + self.task = None + self.slits = [] + + def __repr__(self): + try: + repr_list = [] + for slit in self.slits: + new_slit = slit + new_slit.x = f'{slit.x:.1f}' + new_slit.width = f'{slit.width:.1f}' + repr_list.append(new_slit) + except: + repr_list = None + return f'{repr_list}' + + def set_task(self, task: str): + """Set the current task (calibrate, status, etc.).""" + self.task = task + + def run(self): + """Execute the task based on the worker's task state.""" + if self.task == "calibrate": + self._calibrate() + elif self.task == "status": + self._status() + elif self.task == "configure": + # self.configure_csu() + pass + def update_slits(self,slits): + self.slits = slits + + def _calibrate(self): + """Calibrate the CSU.""" + print("Calibrating CSU...") + try: + response = self.c.calibrate() # Capture the response + logger.debug(f"Calibration Response: {response}") + except TimeoutError as e: + logger.debug(f"Calibration Response: {e}") + print(f"Calibration Response: {e}") + + # Emit calibration response + self.calibrate_signal.emit(response) + + def _status(self): + """Display the current status.""" + try: + response = self.c.status() + self.slits = self.parse_response(response) + logger.debug(f"Status Response: {response}") + except TimeoutError as e: + logger.debug(f"Status Response: {e}") + print(f'Status Response: {e}') + + # Emit slits list + if self.slits: + self.status_signal.emit(self.slits) + + def configure_csu(self, slits): + """Call the CSU's configure method with the slits.""" + print("Configuring csu....") + response = self.c.configure(MaskConfig(slits), speed=6500) + self.slit_config_updated_signal.emit(response) # Emit a signal indicating the configuration has been updated + # self.log_message("Slit configuration updated successfully.") + + def parse_response(self, response): + """Parse the response to extract the mask data.""" + try: + mask_config = response[-1] # Extract mask config from the response + slits = mask_config.slits + return slits + except (IndexError, AttributeError) as e: + error_message = f"Error parsing response: {e}" + self.log_message(error_message) + return [] \ No newline at end of file diff --git a/slitmaskgui/configure_mode/mask_controller.py b/slitmaskgui/configure_mode/mask_controller.py new file mode 100644 index 0000000..5be39a6 --- /dev/null +++ b/slitmaskgui/configure_mode/mask_controller.py @@ -0,0 +1,234 @@ +import sys +from typing import Tuple +from PyQt6.QtWidgets import ( QVBoxLayout, QGraphicsView, QGraphicsScene, + QComboBox, QPushButton, QHBoxLayout, QSplitter, QDialog, QSizePolicy, + QWidget, QGroupBox, QLabel, QLineEdit, QDialogButtonBox, +) +from PyQt6.QtCore import Qt, pyqtSignal, QSize, QTimer, QThreadPool +from PyQt6.QtGui import QPainter +from lris2csu.remote import CSURemote +from lris2csu.slit import Slit, MaskConfig +from slitmaskgui.mask_widgets.mask_objects import ErrorWidget +import time + +from logging import basicConfig, DEBUG, getLogger +from slitmaskgui.configure_mode.csu_worker import CSUWorkerThread # Import the worker thread + + +# basicConfig(filename='mktl.log', format='%(asctime)s %(message)s', filemode='w', level=DEBUG) +basicConfig(level=DEBUG) +getLogger('mktl').setLevel(DEBUG) +logger = getLogger('mktl') + +registry = 'tcp://131.215.200.105:5571' +remote = CSURemote(registry_address=registry) +PLATE_SCALE = 0.7272 +CSU_WIDTH = PLATE_SCALE*60*5 + +publish_socket = "tcp://131.215.200.105:5559" + + +def timeout_function(self, e): + text = f"{e}\nMake sure you are connected to the CSU" + self.error_widget = ErrorWidget(text) + self.error_widget.show() + + +class MaskControllerWidget(QWidget): + connect_with_slitmask_display = pyqtSignal() + def __init__(self): + super().__init__() + self.setSizePolicy( + QSizePolicy.Policy.Ignored, + QSizePolicy.Policy.Ignored + ) + + #------------------------definitions---------------------------- + self.remote_label = QLabel("Registry:") + self.remote_add = QLineEdit(registry) + self.configure_button = QPushButton("Configure") + self.stop_button = QPushButton("Stop") + self.calibrate_button = QPushButton("Calibrate") + self.reset_button = QPushButton("Reset") + self.shutdown_button = QPushButton("Shutdown") + self.status_button = QPushButton("Status") + + self.c = remote + self.worker_thread = CSUWorkerThread(remote) + + self.bar_pairs = [] + self.slits = 0 + #-----------------------------connections--------------------------- + self.configure_button.clicked.connect(self.configure_slits) + self.stop_button.clicked.connect(self.stop_process) + self.calibrate_button.clicked.connect(self.calibrate) + self.reset_button.clicked.connect(self.reset_configuration) + self.shutdown_button.clicked.connect(self.shutdown) + self.status_button.clicked.connect(self.show_status) + + self.worker_thread.calibrate_signal.connect(self.handle_calibration_done) + self.worker_thread.status_signal.connect(self.handle_status_updated) + self.worker_thread.slit_config_updated_signal.connect(self.handle_config_update) + + + #------------------------------------------layout------------------------- + logger.info("mask_gen_widget: defining the layout") + group_box = QGroupBox("CONFIGURATION MODE") + main_layout = QVBoxLayout() + group_layout = QVBoxLayout() + + group_layout.addWidget(self.remote_label) + group_layout.addWidget(self.remote_add) + group_layout.addWidget(self.configure_button) + group_layout.addWidget(self.stop_button) + group_layout.addWidget(self.calibrate_button) + group_layout.addWidget(self.reset_button) + group_layout.addWidget(self.shutdown_button) + group_layout.addWidget(self.status_button) + group_layout.addStretch(40) + group_box.setLayout(group_layout) + + main_layout.setContentsMargins(9,4,9,9) + main_layout.addWidget(group_box) + + self.setLayout(main_layout) + #--------------------- Timer & Threadpool -------------------------- + self.timer = QTimer() + self.timer.setInterval(1500) + self.timer.timeout.connect(self.still_run) + self.old_config = None + #------------------------setting size hints for widgets------------------ + uniform_size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + [ + self.layout().itemAt(i).widget().setSizePolicy(uniform_size_policy) + for i in range(self.layout().count()) + ] + + def sizeHint(self): + return QSize(300,400) + + def connect_controller_with_slitmask_display(self, slitmask_display_class): + self.slitmask_display = slitmask_display_class + self.connect_with_slitmask_display.connect(self.slitmask_display.handle_configuration_mode) + self.slitmask_display.connect_with_controller.connect(self.define_slits) + + + def define_slits(self,slits): + try: + self.slits = slits[:12] + self.slits = tuple([Slit(bar_id,CSU_WIDTH/2+star["x_mm"],float(star["slit_width"])/PLATE_SCALE) # CSU_WIDTH + star because star could be negative + for bar_id,star in enumerate(self.slits)]) + except: + print("no mask config found") + + def configure_slits(self): + try: + self.worker_thread.set_task("configure") + self.worker_thread.configure_csu(self.slits) + except AttributeError as e: + text = f"{e}\nGenerate a Mask Configuration before configuring CSU" + self.error_widget = ErrorWidget(text) + self.error_widget.show() + except TimeoutError as e: + timeout_function(self, e) + def still_run(self): + self.current_config = repr(self.worker_thread) + self.show_status() + + if self.current_config == self.old_config: + self.timer.stop() + + self.old_config = self.current_config + # if self.timer_counter >= self.total_counts: + # self.timer.stop() + # self.timer_counter = 0 + + def reset_configuration(self): + """Reset the configuration to a default state.""" + # Reset to "Stair Mask" + print("Resetting CSU...") + # self.update_slit_configuration() + try: + response = self.c.reset() + except TimeoutError as e: + timeout_function(self, e) + response = e + print(f'reset config {response}') + + def calibrate(self): + """Start the calibration process in the worker thread.""" + print("Starting calibration in worker thread...") + self.worker_thread.set_task("calibrate") + self.worker_thread.start() + + def handle_calibration_done(self, response): + """Handle calibration completion.""" + print(f"Calibration completed: {response}") + self.timer.setInterval(1000) + self.timer.start() + # Update UI accordingly, e.g., show a message or update a label + + def handle_status_updated(self, slits): + """Update GUI with slits returned from CSUWorkerThread.""" + if not slits: + print("No slits received.") + return + + self.slitmask_display.get_slits(slits) + + print("Scene updated with new slit configuration.") + + def handle_error(self, error_message): + """Handle error in the worker thread.""" + print(f"Error occurred: {error_message}") + # Display error in the UI, e.g., using a dialog + + def handle_config_update(self,response): + print(f'Configuration started: {response}') + self.timer.setInterval(750) + self.timer.start() + + + def shutdown(self): + """Shutdown the application.""" + try: + self.c.shutdown() + except TimeoutError as e: + timeout_function(self, e) + + def show_status(self): + """Request slit status from worker thread.""" + print("Requesting status in worker thread...") + self.worker_thread.set_task("status") + self.worker_thread.start() + + def stop_process(self): + """Stop the process by sending the stop command to CSURemote.""" + print("Stopping the process...") + try: + response = self.c.stop() + except TimeoutError as e: + timeout_function(self, e) + response = e + print(f"stop process {response}") + try: + self.timer.stop() + except: + pass #timer already stopped + + + # def parse_response(self, response): + # """Parse the response to extract the mask data.""" + # try: + # # Access the last element of the response to get the MaskConfig object + # mask_config = response[-1] # Using dot notation instead of dictionary access + # slits = mask_config.slits + # log_message = f"Extracted MaskConfig: {mask_config}" + # print(log_message) + # print(f"Slits: {slits}") + # return slits + # except (IndexError, AttributeError) as e: + # # Handle cases where the structure is not as expected + # error_message = f"Error parsing response: {e}" + # print(error_message) + # return None diff --git a/slitmaskgui/configure_mode/mode_toggle_button.py b/slitmaskgui/configure_mode/mode_toggle_button.py new file mode 100644 index 0000000..3e0c540 --- /dev/null +++ b/slitmaskgui/configure_mode/mode_toggle_button.py @@ -0,0 +1,62 @@ +from PyQt6.QtWidgets import QPushButton, QWidget, QVBoxLayout, QDialog, QSizePolicy, QHBoxLayout +from PyQt6.QtCore import pyqtSignal, QSize +from slitmaskgui.configure_mode.csu_worker import CSUWorkerThread +from slitmaskgui.configure_mode.mask_controller import MaskControllerWidget +from lris2csu.remote import CSURemote +import socket + + +""" +will define in a better way later +""" +# remote = CSURemote('tcp://131.215.200.105:5571') +HOST = '131.215.200.105' +PORT = 5571 +conection = 'tcp://131.215.200.105:5571' + + +class ShowControllerButton(QWidget): + get_from_mask_config = pyqtSignal(object) + def __init__(self): + super().__init__() + self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored) + self.button = QPushButton("Configuration Mode (OFF)") + # self.button.clicked.connect(self.on_button_clicked) + self.button.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Maximum) + + + layout = QHBoxLayout() + layout.addWidget(self.button) + self.setLayout(layout) + + def sizeHint(self): + return QSize(100, 60) + + + def connect_controller_with_config(self, mask_controller_class, mask_config_class): #change this to connect to specific class + self.mask_class = mask_config_class + self.controller_class = mask_controller_class + self.get_from_mask_config.connect(self.mask_class.emit_last_used_slitmask) + self.mask_class.send_to_csu.connect(self.controller_class.define_slits) + + def start_communication(self): + self.get_from_mask_config.emit("Start Communication") + + def on_button_clicked(self): + #handle button click + self.start_communication() + # self.check_if_connected() + # def check_if_connected(self): + # try: + # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + # s.connect((HOST, PORT)) + # s.send(b"some data") + # self.controller_class.connection_status.setText('Connection Status:\nCONNECTED') + # except ConnectionRefusedError as e: + # self.controller_class.connection_status.setText('Connection Status:\nCSU NOT CONNECTED') + + + + + + diff --git a/slitmaskgui/mask_configurations.py b/slitmaskgui/mask_configurations.py index e3b329a..a2581ab 100644 --- a/slitmaskgui/mask_configurations.py +++ b/slitmaskgui/mask_configurations.py @@ -11,22 +11,17 @@ from astropy.coordinates import SkyCoord,Angle import astropy.units as u from slitmaskgui.backend.star_list import StarList -from PyQt6.QtCore import Qt, QAbstractTableModel,QSize, QModelIndex, pyqtSlot, pyqtSignal +from PyQt6.QtCore import Qt, QAbstractTableModel,QSize, QModelIndex, pyqtSignal from PyQt6.QtWidgets import ( - QApplication, - QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, - QLabel, QPushButton, QGroupBox, QTableView, QSizePolicy, QHeaderView, QFileDialog, - - ) config_logger = logging.getLogger(__name__) @@ -52,15 +47,10 @@ def __init__(self, data=[]): def headerData(self, section, orientation, role = ...): if role == Qt.ItemDataRole.DisplayRole: - #should add something about whether its vertical or horizontal if orientation == Qt.Orientation.Horizontal: - return self.headers[section] - if orientation == Qt.Orientation.Vertical: return None - - return super().headerData(section, orientation, role) def data(self, index, role): @@ -80,6 +70,7 @@ def removeRow(self, row, count=1, parent=QModelIndex()): def get_num_rows(self): return len(self._data) + def get_row_num(self,index): if len(index) > 0: return index[0].row() @@ -87,16 +78,15 @@ def get_row_num(self,index): def rowCount(self, index): return len(self._data) + def columnCount(self, index): return 2 def setData(self, index, value, role = ...): - if role == Qt.ItemDataRole.EditRole: - # Set the value into the frame. + if role == Qt.ItemDataRole.DisplayRole: self._data[index.row()][index.column()] = value - self.dataChanged.emit(index, index) + self.dataChanged.emit(index,index) return True - return False class CustomTableView(QTableView): @@ -104,12 +94,10 @@ def __init__(self): super().__init__() self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(0) - #self.setEditTriggers(QTableView.EditTrigger.DoubleClicked) self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) self.setSelectionMode(QTableView.SelectionMode.SingleSelection) - def setResizeMode(self): self.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) @@ -117,11 +105,7 @@ def setResizeMode(self): def setModel(self, model): super().setModel(model) self.setResizeMode() - - - return 2 - - + return 2 # I don't know what this does at all @@ -131,10 +115,12 @@ class MaskConfigurationsWidget(QWidget): change_slit_image = pyqtSignal(dict) change_row_widget = pyqtSignal(list) reset_scene = pyqtSignal(bool) - update_image = pyqtSignal(np.ndarray) - data_to_save_request = pyqtSignal(object) + update_image = pyqtSignal(object) + data_to_save_request = pyqtSignal() changes_have_been_saved = pyqtSignal(object) change_name_above_slit_mask = pyqtSignal(np.ndarray) + + send_to_csu = pyqtSignal(object) def __init__(self): super().__init__() @@ -157,16 +143,14 @@ def __init__(self): self.table = CustomTableView() self.model = TableModel() self.table.setModel(self.model) + # maybe have the current row number as a self.row + self.row_to_config_dict = {} + self.last_used_slitmask = [] #------------------------connections----------------- - self.open_button.clicked.connect(self.open_button_clicked) - self.save_button.clicked.connect(self.save_button_clicked) - self.close_button.clicked.connect(self.close_button_clicked) - self.export_button.clicked.connect(self.export_button_clicked) - self.export_all_button.clicked.connect(self.export_all_button_clicked) - self.table.selectionModel().selectionChanged.connect(self.selected) #sends the row number for the selected item + self.connect_signalers() #-------------------layout------------------- group_box = QGroupBox("MASK CONFIGURATIONS") @@ -195,35 +179,35 @@ def __init__(self): group_box.setLayout(group_layout) group_box.setContentsMargins(2,0,2,0) - # main_layout.addWidget(title,alignment=Qt.AlignmentFlag.AlignHCenter) main_layout.addWidget(group_box) main_layout.setSpacing(0) main_layout.setContentsMargins(9,4,9,9) self.setLayout(main_layout) #------------------------------------------------ + def sizeHint(self): return QSize(300,150) - - def is_connected(self,connect:bool): - if connect: - self.open_button.clicked.connect(self.open_button_clicked) - self.save_button.clicked.connect(self.save_button_clicked) - self.close_button.clicked.connect(self.close_button_clicked) - self.export_button.clicked.connect(self.export_button_clicked) - self.export_all_button.clicked.connect(self.export_all_button_clicked) - self.table.selectionModel().selectionChanged.connect(self.selected) #sends the row number for the selected item - else: - self.open_button.clicked.disconnect(self.open_button_clicked) - self.save_button.clicked.disconnect(self.save_button_clicked) - self.close_button.clicked.disconnect(self.close_button_clicked) - self.export_button.clicked.disconnect(self.export_button_clicked) - self.export_all_button.clicked.disconnect(self.export_all_button_clicked) - self.table.selectionModel().selectionChanged.disconnect(self.selected) #sends the row number for the selected item + + def connect_signalers(self): + self.open_button.clicked.connect(self.open_button_clicked) + self.save_button.clicked.connect(self.save_button_clicked) + self.close_button.clicked.connect(self.close_button_clicked) + self.export_button.clicked.connect(self.export_button_clicked) + self.export_all_button.clicked.connect(self.export_all_button_clicked) + self.table.selectionModel().selectionChanged.connect(self.selected) + + def disconnect_signalers(self): + self.open_button.clicked.disconnect(self.open_button_clicked) + self.save_button.clicked.disconnect(self.save_button_clicked) + self.close_button.clicked.disconnect(self.close_button_clicked) + self.export_button.clicked.disconnect(self.export_button_clicked) + self.export_all_button.clicked.disconnect(self.export_all_button_clicked) + self.table.selectionModel().selectionChanged.disconnect(self.selected) def open_button_clicked(self): - config_logger.info(f"mask configurations: start of open button function {self.row_to_config_dict}") + config_logger.info(f"Mask Configuration Widget: open button clicked") file_path, _ = QFileDialog.getOpenFileName( self, @@ -231,39 +215,28 @@ def open_button_clicked(self): "", "All files (*)" #will need to make sure it is a specific file ) - #update this with the row to json dict thing - try: - if file_path: - with open(file_path,'r') as f: - temp = f.read() - data = json.loads(temp) - mask_name = os.path.splitext(os.path.basename(file_path))[0] - self.update_table((mask_name,data)) #doesn't work right now - config_logger.info(f"mask_configurations: open button clicked {mask_name} {file_path}") - #in the future this will take the mask config file and take the name from that file and display it - #it will also auto select itself and display the mask configuration on the interactive slit mask - except: - pass - config_logger.info(f"mask configurations: end of open button function {self.row_to_config_dict}") - - - def save_button_clicked(self,item): - self.data_to_save_request.emit(None) + if file_path: + with open(file_path,'r') as f: + temp = f.read() + data = json.loads(temp) + mask_name = os.path.splitext(os.path.basename(file_path))[0] + self.initialize_configuration((mask_name,data)) + config_logger.info(f"Mask Configuration Widget: File path selected {mask_name} {file_path}") + + + def save_button_clicked(self): + self.data_to_save_request.emit() def save_data_to_mask(self,new_data): - data = new_data - if data[0]: #if data has actually been changed + if new_data: row_num = self.model.get_row_num(self.table.selectedIndexes()) - for x in self.row_to_config_dict[row_num]: - bar_id = x["bar_id"] - if bar_id in data[1]: - x["slit_width"] = data[1][bar_id] - self.update_table(row=row_num) - + for bar in self.row_to_config_dict[row_num]: + if bar["bar_id"] in new_data: + bar["slit_width"] = new_data[bar["bar_id"]] + self.update_table_to_saved(row_num) - def close_button_clicked(self,item): - #this will delete the item from the list and the information that goes along with it - #get selected item + + def close_button_clicked(self): config_logger.info(f"mask configurations: start of close button function {self.row_to_config_dict}") row_num = self.model.get_row_num(self.table.selectedIndexes()) if row_num is None: @@ -279,78 +252,62 @@ def close_button_clicked(self,item): self.reset_scene.emit(True) - def export_button_clicked(self): #should probably change to export to avoid confusion with saved/unsaved which is actually updated/notupdated - #this will save the current file selected in the table + def export_button_clicked(self): config_logger.info(f"mask configurations: start of export button function {self.row_to_config_dict}") - row_num = self.model.get_row_num(self.table.selectedIndexes()) #this gets the row num + row_num = self.model.get_row_num(self.table.selectedIndexes()) try: index = self.model.index(row_num, 1) name = self.model.data(index,Qt.ItemDataRole.DisplayRole) - except: - #index has recieved nonetype - pass - if row_num is None: - config_logger.warning("No row selected for export.") - return - else: + file_path, _ = QFileDialog.getSaveFileName( self, "Save File", f"{name}", "JSON Files (*.json)" ) + print("file_path_done") if file_path: - data = json.dumps(self.row_to_config_dict[row_num]) + data = self.row_to_config_dict[row_num] ra, dec = self.get_center(data) - star_list = StarList(data,ra=ra,dec=dec,slit_width=0.7,auto_run=False) - mask_name = os.path.splitext(os.path.basename(file_path)) + star_list = StarList(json.dumps(data),ra=ra,dec=dec,slit_width=0.7,auto_run=False) star_list.export_mask_config(file_path=file_path) - + except TypeError as e: + print(f'{e}\nNo mask configurations found') config_logger.info(f"mask configurations: end of export button function {self.row_to_config_dict}") - def export_all_button_clicked(self): - #this will export all files - #you will choose a directory for all the files to go to and then all the files will be automatically named - #mask_name.json and will be saved in that directory row_num = self.model.get_row_num(self.table.selectedIndexes()) - pyqtSlot(name="update_table") - def update_table(self,info=None,row=None): - #the first if statement is for opening a mask file and making a mask in the gui which will be automatically added - config_logger.info(f"mask configurations: start of update table function {self.row_to_config_dict}") - - if info is not None: #info for now will be a list [name,file_path] - name, mask_info = info[0], info[1] - self.model.beginResetModel() - self.model._data.append(["Saved",name]) - self.model.endResetModel() - row_num = self.model.get_num_rows() -1 - self.row_to_config_dict.update({row_num: mask_info}) - self.table.selectRow(row_num) - elif row: - config_logger.info(f"mask configurations: changes have been saved to {self.model._data[row][1]}") - self.model.beginResetModel() - self.model._data[row] = ["Saved",self.model._data[row][1]] - self.model.endResetModel() - else: + def initialize_configuration(self,config): + config_logger.info(f'Mask Configuration Widget: initializing mask configuration') + name, mask_info = config[0], config[1] + self.model.beginResetModel() + self.model._data.append(["Saved",name]) + self.model.endResetModel() + row_num = self.model.get_num_rows() - 1 + self.row_to_config_dict.update({row_num: mask_info}) + self.table.selectRow(row_num) + + def update_table_to_saved(self,row): + config_logger.info(f"mask configurations: changes have been saved to {self.model._data[row][1]}") + index = self.model.index(row,0) + self.model.setData(index, "Saved", Qt.ItemDataRole.DisplayRole) + self.table.selectRow(row) + + def update_table_to_unsaved(self): + try: config_logger.info(f'mask configurations: new data added but is unsaved') - try: - row_num = self.model.get_row_num(self.table.selectedIndexes()) - self.model.beginResetModel() - self.model._data[row_num] = ["Unsaved",self.model._data[row_num][1]] - self.model.endResetModel() - self.is_connected(False) - self.table.selectRow(row_num) - self.is_connected(True) - except: - config_logger.info(f'mask configurations: there are no rows') - config_logger.info(f"mask configurations: end of update table function {self.row_to_config_dict}") - # when a mask configuration is run, this will save the data in a list - @pyqtSlot(name="selected file path") + current_row = self.model.get_row_num(self.table.selectedIndexes()) + index = self.model.index(current_row,0) + self.model.setData(index, "Unsaved", Qt.ItemDataRole.DisplayRole) + self.connect_signalers() + self.table.selectRow(current_row) + self.disconnect_signalers() + except TypeError as e: + config_logger.info(f'Mask Configuration Widget: {e}') + def selected(self): - #will update the slit mask depending on which item is selected - if len(self.row_to_config_dict) >0: + if len(self.row_to_config_dict) > 0: row = self.model.get_row_num(self.table.selectedIndexes()) config_logger.info(f"mask_configurations: row is selected function {row} {self.row_to_config_dict}") data = json.dumps(self.row_to_config_dict[row]) @@ -362,14 +319,19 @@ def selected(self): interactive_slit_mask = slit_mask.send_interactive_slit_list() self.change_slit_image.emit(interactive_slit_mask) - self.change_data.emit(slit_mask.send_target_list()) self.change_row_widget.emit(slit_mask.send_row_widget_list()) - self.update_image.emit(slit_mask.generate_skyview()) + mask_name_info = np.array([str(name),str(str(ra)+str(dec)),str(pa)]) self.change_name_above_slit_mask.emit(mask_name_info) + + self.last_used_slitmask = slit_mask.send_mask() + self.emit_last_used_slitmask() + + self.update_image.emit(slit_mask) def get_center(self,star_data): + """neccessary in case someone imports a file (file doesn't contain the center)""" star = star_data[0] #make first star into a coordinate coord = SkyCoord(star["ra"],star["dec"], unit=(u.hourangle, u.deg), frame='icrs') @@ -382,9 +344,14 @@ def get_center(self,star_data): #format it back into hourangle degree center_ra = Angle(new_ra).to_string(unit=u.hourangle, sep=' ', precision=2, pad=True) center_dec = Angle(new_dec).to_string(unit=u.deg, sep=' ', precision=2, pad=True,alwayssign=True) - #return it + return center_ra,center_dec + def emit_last_used_slitmask(self): + self.send_to_csu.emit(self.last_used_slitmask) + + + diff --git a/slitmaskgui/mask_gen_widget.py b/slitmaskgui/mask_gen_widget.py index aa6e03b..d2dfafe 100644 --- a/slitmaskgui/mask_gen_widget.py +++ b/slitmaskgui/mask_gen_widget.py @@ -1,52 +1,33 @@ from slitmaskgui.backend.input_targets import TargetList from slitmaskgui.backend.star_list import StarList +from slitmaskgui.mask_widgets.mask_objects import ErrorWidget import re import logging import numpy as np -from PyQt6.QtCore import QObject, pyqtSignal, Qt, QSize +from PyQt6.QtCore import pyqtSignal, Qt, QSize, QThread from PyQt6.QtWidgets import ( QFileDialog, QVBoxLayout, QWidget, QPushButton, - QStackedLayout, QLineEdit, QFormLayout, QGroupBox, - QBoxLayout, QSizePolicy, - QGridLayout, QHBoxLayout, QLabel, - QLayout, QCheckBox, QDialog, - QDialogButtonBox ) #need to add another class to load parameters from a text file logger = logging.getLogger(__name__) -class ErrorWidget(QDialog): - def __init__(self,dialog_text): - super().__init__() - self.setWindowTitle("ERROR") - layout = QVBoxLayout() - self.setWindowModality(Qt.WindowModality.ApplicationModal) - self.setWindowFlags( - self.windowFlags() | - Qt.WindowType.WindowStaysOnTopHint - ) - - self.label = QLabel(dialog_text) - buttons = QDialogButtonBox.StandardButton.Ok - button_box = QDialogButtonBox(buttons) - button_box.accepted.connect(self.accept) - layout.addWidget(self.label) - layout.addWidget(button_box) - self.setLayout(layout) + + + class MaskGenWidget(QWidget): change_data = pyqtSignal(list) @@ -171,15 +152,12 @@ def run_button(self): interactive_slit_mask = slit_mask.send_interactive_slit_list() if interactive_slit_mask: - self.change_slit_image.emit(interactive_slit_mask) - self.change_data.emit(slit_mask.send_target_list()) self.change_row_widget.emit(slit_mask.send_row_widget_list()) logger.info("mask_gen_widget: sending mask config to mask_configurations") self.send_mask_config.emit([mask_name,slit_mask.send_mask(mask_name=mask_name)]) #this is temporary I have no clue what I will actually send back (at le¡ast the format of it) self.change_wavelength_data.emit(slit_mask.send_list_for_wavelength()) - # self.update_image.emit(slit_mask.generate_skyview()) #-------------------------------------------------------------------------- else: self.error_catching() @@ -190,6 +168,8 @@ def error_catching(self): self.error_widget.show() if self.error_widget.exec() == QDialog.DialogCode.Accepted: pass + + diff --git a/slitmaskgui/mask_view_tab_bar.py b/slitmaskgui/mask_view_tab_bar.py deleted file mode 100644 index eab9e05..0000000 --- a/slitmaskgui/mask_view_tab_bar.py +++ /dev/null @@ -1,76 +0,0 @@ -# import logging -# import numpy as np -# from astroquery.gaia import Gaia -# from astropy.coordinates import SkyCoord -# import astropy.units as u -# from PyQt6.QtCore import Qt, pyqtSlot, pyqtSignal, QSize -# from PyQt6.QtGui import QBrush, QPen, QPainter, QColor, QFont, QTransform -# from slitmaskgui.mask_viewer import interactiveSlitMask, WavelengthView - -from PyQt6.QtCore import pyqtSignal, Qt, QPoint, QTimer, QItemSelectionModel -from PyQt6.QtWidgets import ( - QTabWidget, - QComboBox, - QLabel, - QVBoxLayout, - QWidget, - QListView - -) -class CustomComboBox(QComboBox): - def __init__(self): - super().__init__() - items = ['phot_bp_mean_mag', 'phot_g_mean_mag', 'phot_rp_mean_mag'] - self.addItems(items) - - def showPopup(self): - popup = self.view().window() - if popup.isVisible(): - popup.hide() - - super().showPopup() - - pos = self.mapToGlobal(QPoint(0, self.height())) - popup.move(pos) - popup.show() - -class TabBar(QTabWidget): - waveview_change = pyqtSignal(int) - def __init__(self,slitmask,waveview,skyview): - super().__init__() - #--------------defining widgets for tabs--------- - self.wavelength_view = QLabel("Spectral view is currently under development")#waveview #currently waveview hasn't been developed - self.interactive_slit_mask = slitmask - self.sky_view = skyview - - #--------------defining comobox------------------ - self.combobox = CustomComboBox() - - #--------------defining tabs-------------- - self.addTab(self.interactive_slit_mask,"Slit Mask") - self.addTab(self.wavelength_view,"Spectral View") - self.addTab(self.sky_view,"Sky View") - - self.setCornerWidget(self.combobox) - self.combobox.hide() - # self.mask_tab.setCornerWidget(self.combobox) #this would add the widget to the corner (only want it when spectral view is selected) - #------------------other--------------- - self.wavelength_view.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.wavelength_view.setStyleSheet("font-size: 20px;") - - #------------------connections------------ - self.tabBar().currentChanged.connect(self.wavetab_selected) - self.combobox.currentIndexChanged.connect(self.send_to_view) - # self.tabBar().currentChanged.connect() - - - def wavetab_selected(self,selected): - if selected == 1: - self.combobox.show() - else: - self.combobox.hide() - - def send_to_view(self,index): - self.waveview_change.emit(index) - - diff --git a/slitmaskgui/mask_viewer.py b/slitmaskgui/mask_viewer.py deleted file mode 100644 index 13dbc91..0000000 --- a/slitmaskgui/mask_viewer.py +++ /dev/null @@ -1,458 +0,0 @@ -""" -This is the interactive slit mask feature. It will interact with the bar table on the left. -when you click the bar on the left then the image will display which row that is -additionally It will also interact with the target list -it will display where the slit is place and what stars will be shown -""" - - -import matplotlib.pyplot as plt -import logging -import numpy as np -from astroquery.gaia import Gaia -from astropy.coordinates import SkyCoord -import astropy.units as u -import matplotlib.pyplot as plt -from astropy.wcs import WCS -from astropy.io import fits -from PyQt6.QtCore import Qt, pyqtSlot, pyqtSignal, QSize -from PyQt6.QtGui import QBrush, QPen, QPainter, QColor, QFont, QTransform -from PyQt6.QtWidgets import ( - QVBoxLayout, - QHBoxLayout, - QWidget, - QLabel, - QGraphicsView, - QGraphicsScene, - QGraphicsRectItem, - QGraphicsLineItem, - QGraphicsTextItem, - QGraphicsItemGroup, - QSizePolicy, - QSizeGrip, - QTabWidget, - QComboBox - - -) - -#will have another thing that will dispaly all the stars in the sky at the time -PLATE_SCALE = 0.7272 #(mm/arcsecond) on the sky -CSU_HEIGHT = PLATE_SCALE*60*10 #height of csu in mm (height is 10 arcmin) -CSU_WIDTH = PLATE_SCALE*60*5 #width of the csu in mm (widgth is 5 arcmin) -MM_TO_PIXEL = 1 #this is a mm to pixel ratio, it is currently just made up - -logger = logging.getLogger(__name__) - - - -#got to add a "if dark mode then these are the colors" - -class interactiveBars(QGraphicsRectItem): - - def __init__(self,x,y,bar_length,bar_width,this_id): - super().__init__() - #creates a rectangle that can cha - self.length = bar_length - self.width = bar_width - self.y_pos = y - self.setRect(x,self.y_pos, self.length,self.width) - self.id = this_id - self.setBrush = QBrush(Qt.BrushStyle.NoBrush) - self.setPen = QPen(QColor.fromString("#6c7086")).setWidth(1) - self.setFlags(self.GraphicsItemFlag.ItemIsSelectable) - - - def check_id(self): - return self.id - - def paint(self, painter: QPainter, option, widget = None): - if self.isSelected(): - #self.setBrush = QBrush(Qt.GlobalColor.blue) - painter.setBrush(QBrush(QColor.fromString("#89b4fa"))) - painter.setPen(QPen(QColor.fromString("#1e1e2e"), 0)) - else: - painter.setBrush(QBrush(Qt.BrushStyle.NoBrush)) - painter.setPen(QPen(QColor.fromString("#6c7086"), 0)) - painter.drawRect(self.rect()) - - def send_size(self): - return (self.length,self.width) - -class FieldOfView(QGraphicsRectItem): - def __init__(self,height=CSU_HEIGHT*MM_TO_PIXEL,width=CSU_WIDTH*MM_TO_PIXEL,x=0,y=0): - super().__init__() - - self.height = height - self.width = width #ratio of height to width - - self.setRect(x,y,self.width,self.height) - - self.setPen(QPen(QColor.fromString("#a6e3a1"),4)) - self.setFlags(self.flags() & ~self.GraphicsItemFlag.ItemIsSelectable) - - self.setOpacity(0.5) - def change_height(self): - pass - - -class interactiveSlits(QGraphicsItemGroup): - - def __init__(self,x,y,name="NONE"): - super().__init__() - #line length will be dependent on the amount of slits - #line position will depend on the slit position of the slits (need to check slit width and postion) - #will have default lines down the middle - #default NONE next to lines that don't have a star - self.x_pos = x - self.y_pos = y - self.line = QGraphicsLineItem(self.x_pos,self.y_pos,self.x_pos,self.y_pos+CSU_HEIGHT/72) - #self.line = QLineF(x,y,x,y+7) - self.line.setPen(QPen(QColor.fromString("#eba0ac"), 2)) - - self.star_name = name - self.star = QGraphicsTextItem(self.star_name) - self.star.setDefaultTextColor(QColor.fromString("#eba0ac")) - self.star.setFont(QFont("Arial",6)) - self.star.setPos(x+5,y-4) - self.setFlags(self.flags() & ~self.GraphicsItemFlag.ItemIsSelectable) - - - self.addToGroup(self.line) - self.addToGroup(self.star) - def get_y_value(self): - return self.y_pos - def get_bar_id(self): - return int(self.y_pos/(CSU_HEIGHT*MM_TO_PIXEL/72)) - def get_star_name(self): - return self.star_name - -class CustomGraphicsView(QGraphicsView): - def __init__(self,scene): - super().__init__(scene) - # self.scene() == scene - self.previous_height = self.height() - self.previous_width = self.width() - - self.scale_x = 1.8 - self.scale_y = 1.8 #0.9 - - self.scale(self.scale_x, self.scale_y) - - self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - - self.fitInView(scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) - self.setViewportMargins(0,0,0,0) - - def resizeEvent(self,event): - super().resizeEvent(event) - self.fitInView(self.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) - - def sizePolicy(self): - return super().sizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - def renderHints(self): - return super().renderHints(QPainter.RenderHint.Antialiasing) - -class interactiveSlitMask(QWidget): - row_selected = pyqtSignal(int,name="row selected") - select_star = pyqtSignal(str) - - def __init__(self): - super().__init__() - - #--------------------definitions----------------------- - logger.info("slit_view: doing definitions") - self.scene_width = (CSU_WIDTH+CSU_WIDTH/1.25) * MM_TO_PIXEL - scene_height = CSU_HEIGHT * MM_TO_PIXEL - self.scene = QGraphicsScene(0,0,self.scene_width,scene_height) - - xcenter_of_image = self.scene.sceneRect().center().x() - - self.mask_name_title = QLabel(f'MASK NAME: None') - self.center_title = QLabel(f'CENTER: None') - self.pa_title = QLabel(f'PA: None') - - bar_length = self.scene_width - self.bar_height = CSU_HEIGHT/72#PLATE_SCALE*8.6 - padding = 0 - - for i in range(72): - temp_rect = interactiveBars(0,i*self.bar_height+padding,this_id=i,bar_width=self.bar_height,bar_length=bar_length) - temp_slit = interactiveSlits(self.scene_width/2,self.bar_height*i+padding) - self.scene.addItem(temp_rect) - self.scene.addItem(temp_slit) - - fov = FieldOfView(x=xcenter_of_image/2,y=padding) - new_center = fov.boundingRect().center().x() - new_x = xcenter_of_image-new_center - fov.setPos(new_x,0) - self.scene.addItem(fov) - - self.scene.setSceneRect(self.scene.itemsBoundingRect()) - self.view = CustomGraphicsView(self.scene) - self.view.setContentsMargins(0,0,0,0) - - #-------------------connections----------------------- - logger.info("slit_view: establishing connections") - self.scene.selectionChanged.connect(self.row_is_selected) - self.scene.selectionChanged.connect(self.get_star_name_from_row) - - - #------------------------layout----------------------- - logger.info("slit_view: defining layout") - top_layout = QHBoxLayout() - main_layout = QVBoxLayout() - - - top_layout.addWidget(self.mask_name_title,alignment=Qt.AlignmentFlag.AlignHCenter) - top_layout.addWidget(self.center_title,alignment=Qt.AlignmentFlag.AlignHCenter) - top_layout.addWidget(self.pa_title,alignment=Qt.AlignmentFlag.AlignHCenter) - main_layout.addLayout(top_layout) - main_layout.setSpacing(0) - main_layout.setContentsMargins(0,0,0,0) - main_layout.addWidget(self.view) - - self.setLayout(main_layout) - #------------------------------------------- - def sizeHint(self): - return QSize(550,620) - def connect_on(self,answer:bool): - #---------------reconnect connections--------------- - if answer: - self.scene.selectionChanged.connect(self.row_is_selected) - self.scene.selectionChanged.connect(self.get_star_name_from_row) - else: - self.scene.selectionChanged.disconnect(self.row_is_selected) - self.scene.selectionChanged.disconnect(self.get_star_name_from_row) - @pyqtSlot(int,name="row selected") - def select_corresponding_row(self,row): - logger.info("slit_view: method select_correspond_row called") - - all_bars = [ - item for item in reversed(self.scene.items()) - if isinstance(item, QGraphicsRectItem) - ] - - self.scene.clearSelection() - # self.connect_on(False) - if 0 <= row 0: - row_num = self.scene.selectedItems()[0].check_id() - self.row_selected.emit(row_num) - - @pyqtSlot(list,name="wavelength data") - def get_spectra_of_star(self,ra_dec_list): #[bar_id,ra,dec] - self.spectra_dict = {} - for x in ra_dec_list: - bar_id = x[0] - ra = x[1] - dec = x[2] - coord = SkyCoord(ra, dec, unit=(u.hourangle, u.deg), frame='icrs') - #Currently not available - - self.re_initialize_scene(0) - #gets the flux of each - pyqtSlot(int,name="re-initializing scene") - def re_initialize_scene(self,index): - slit_spacing = 7 - - try: - new_items = [ - interactiveSlits(x=240, y=bar_id * slit_spacing + 7, name=str(np.float32(value[index]))) - for bar_id, value in self.spectra_dict.items() - ] - [self.scene.removeItem(item) for item in reversed(self.scene.items()) if isinstance(item, QGraphicsItemGroup)] - - except: - return - - for item in new_items: - self.scene.addItem(item) - - self.view.setScene(self.scene) - - - @pyqtSlot(np.ndarray, name="update labels") - def update_name_center_pa(self,info): - mask_name, center, pa = info[0], info[1], info[2] #the format of info is [mask_name,center,pa] - self.mask_name_title.setText(f'MASK NAME: {mask_name}') - self.center_title.setText(f'CENTER: {center}') - self.pa_title.setText(f'PA: {pa}') - - - -# Define the coordinates (RA, Dec) - replace with your values - diff --git a/slitmaskgui/mask_widgets/mask_objects.py b/slitmaskgui/mask_widgets/mask_objects.py new file mode 100644 index 0000000..28fd383 --- /dev/null +++ b/slitmaskgui/mask_widgets/mask_objects.py @@ -0,0 +1,391 @@ + +import logging +from PyQt6.QtCore import Qt, QPointF, pyqtProperty, QRectF +from PyQt6.QtGui import QBrush, QPen, QPainter, QColor, QFont, QLinearGradient +from PyQt6.QtWidgets import ( + QGraphicsView, + QGraphicsRectItem, + QGraphicsLineItem, + QGraphicsTextItem, + QGraphicsItemGroup, + QSizePolicy, + QApplication, + QGraphicsObject, + QVBoxLayout, + QDialogButtonBox, + QLabel, + QDialog + + +) + +#taken from https://catppuccin.com/palette/ + +dark_palette = { + 'green': "#a6e3a1", + 'blue': "#89b4fa", + 'sapphire': "#74c7ec", + 'base': "#1e1e2e", + 'overlay_0': "#6c7086", + 'overlay_2': "#9399b2", + 'maroon': "#eba0ac", + 'text': "#cdd6f4", +} +light_palette = { + 'green': "#40a02b", + 'blue': "#1e66f5", + 'sapphire': "#209fb5", + 'base': "#eff1f5", + 'overlay_0': "#7c7f93", #switched with overlay 2 + 'overlay_2': "#9ca0b0", #switched with overlay 0 + 'maroon': "#e64553", + 'text': "#4c4f69" +} + + +def get_theme() -> dict: + theme = QApplication.instance().styleHints().colorScheme() + if theme == Qt.ColorScheme.Dark: + return dark_palette + elif theme == Qt.ColorScheme.Light: + return light_palette + return dark_palette + + + +#will have another thing that will dispaly all the stars in the sky at the time +PLATE_SCALE = 0.7272 #(mm/arcsecond) on the sky +CSU_HEIGHT = PLATE_SCALE*60*10 #height of csu in mm (height is 10 arcmin) +CSU_WIDTH = PLATE_SCALE*60*5 #width of the csu in mm (widgth is 5 arcmin) +MM_TO_PIXEL = 1 #this is a mm to pixel ratio, it is currently just made up +CCD_HEIGHT = 61.2 #in mm +CCD_WIDTH = 61.2 #in mm +DEMO_WIDTH = 260 +DEMO_HEIGHT = 75 + + +logger = logging.getLogger(__name__) + + +class ErrorWidget(QDialog): + def __init__(self,dialog_text): + super().__init__() + self.setWindowTitle("ERROR") + layout = QVBoxLayout() + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setWindowFlags( + self.windowFlags() | + Qt.WindowType.WindowStaysOnTopHint + ) + + self.label = QLabel(dialog_text) + buttons = QDialogButtonBox.StandardButton.Ok + button_box = QDialogButtonBox(buttons) + button_box.accepted.connect(self.accept) + layout.addWidget(self.label) + layout.addWidget(button_box) + self.setLayout(layout) + +class SimpleTextItem(QGraphicsTextItem): + def __init__(self,text): + super().__init__() + self.setPlainText(text) + self.theme = get_theme() + self.setDefaultTextColor(QColor(self.theme['text'])) + self.setFont(QFont("Arial",1)) + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + + def update_theme(self): + self.theme = get_theme() + +class SimpleBarPair(QGraphicsObject): + def __init__(self, slit_width: float, x_position: float, bar_id: int, left_side: bool = True): + super().__init__() + self.bar_length = DEMO_WIDTH # I will fact check this + self.bar_height = DEMO_HEIGHT/12 # I will change this later + + self.slit_width = slit_width # needs to be in mm + self.x_pos = x_position - self.slit_width/2 + self.y_pos = bar_id * self.bar_height + self.theme = get_theme() + self.side = left_side + self.setPos(self.x_pos,self.y_pos) + #I might paint differently depending on themes + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + def update_theme(self): + self.theme = get_theme() + # self.setPen(QPen(QColor.fromString(self.theme['green']),self.thickness)) + def paint(self, painter: QPainter, option, widget = None): + painter.setBrush(QBrush(QColor.fromString(self.theme['overlay_2']))) + painter.setPen(QPen(QColor.fromString(self.theme['overlay_0']), 1)) + if self.side: + painter.drawRect(self.left_side()) + else: + painter.drawRect(self.right_side()) + def left_side(self): + rect_item = QRectF(0,0, - self.bar_length, self.bar_height) + return rect_item + def right_side(self): + rect_item = QRectF(self.slit_width,0, self.bar_length, self.bar_height) + return rect_item + def boundingRect(self): + if self.side: + return self.left_side() + else: + return self.right_side() + def get_pos(self): + return QPointF(self.x(), self.slit_width) + def set_pos(self, pos: QPointF): + self.setX(pos.x()) + self.slit_width = pos.y() + + pos_anim = pyqtProperty(QPointF, fget=get_pos, fset=set_pos) + +class MoveableFieldOfView(QGraphicsObject): + def __init__(self,height=DEMO_HEIGHT,width=DEMO_WIDTH,x=0,y=0,thickness = 4): + super().__init__() + + self.theme = get_theme() + self.width = width + self.height = height + + self.setPos(x,y) + self.thickness = thickness + + # self.setOpacity(0.5) + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + def paint(self, painter: QPainter, option, widget = None): + painter.setPen(QPen(QColor.fromString(self.theme['green']),self.thickness)) + painter.drawRect(self.rect()) + def rect(self): + rect_item = QRectF(0,0, self.width, self.height) + return rect_item + def update_theme(self): + self.theme = get_theme() + def boundingRect(self): + return self.rect() + def get_pos(self): + return self.pos() + def set_pos(self, pos: QPointF): + self.setPos(pos) + + pos_anim = pyqtProperty(QPointF, fget=get_pos, fset=set_pos) + + +class interactiveBars(QGraphicsRectItem): + + def __init__(self,x,y,bar_length,bar_width,this_id,has_gradient=False): + super().__init__() + #creates a rectangle that can cha + self.length = bar_length + self.width = bar_width + self.y_pos = y + self.x_pos = x + self.has_gradient = has_gradient + self.setRect(self.x_pos,self.y_pos, self.length,self.width) + self.id = this_id + self.setFlags(self.GraphicsItemFlag.ItemIsSelectable) + self.theme = get_theme() + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + + def update_theme(self): + self.theme = get_theme() + + def check_id(self): + return self.id + + def paint(self, painter: QPainter, option, widget = None): + + if self.has_gradient: + gradient = self.draw_with_gradient() + painter.setBrush(QBrush(gradient)) + if self.isSelected(): + painter.setPen(QPen(QColor.fromString(self.theme['blue']), 0)) + else: + painter.setPen(QPen(QColor.fromString(self.theme['base']), 0)) + elif self.isSelected(): + painter.setBrush(QBrush(QColor.fromString(self.theme['blue']))) + painter.setPen(QPen(QColor.fromString(self.theme['base']), 0)) + else: + painter.setBrush(QBrush(Qt.BrushStyle.NoBrush)) + painter.setPen(QPen(QColor.fromString(self.theme['overlay_0']), 0)) + + painter.drawRect(self.rect()) + + def draw_with_gradient(self): + start_point = QPointF(self.x_pos, self.y_pos) + end_point = QPointF(self.x_pos+self.length, self.y_pos +self.width) + + gradient = QLinearGradient(start_point, end_point) + gradient.setColorAt(0.0, QColor(self.theme['overlay_0'])) + gradient.setColorAt(1.0, QColor(self.theme['overlay_2'])) + return gradient + + def send_size(self): + return (self.length,self.width) + + +class FieldOfView(QGraphicsRectItem): + def __init__(self,height=CSU_HEIGHT*MM_TO_PIXEL,width=CSU_WIDTH*MM_TO_PIXEL,x=0,y=0,thickness = 4): + super().__init__() + + self.theme = get_theme() + + self.setRect(x,y,width,height) + self.thickness = thickness + + self.setPen(QPen(QColor.fromString(self.theme['green']),self.thickness)) + self.setFlags(self.flags() & ~self.GraphicsItemFlag.ItemIsSelectable) + + self.setOpacity(0.5) + + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + + def update_theme(self): + self.theme = get_theme() + self.setPen(QPen(QColor.fromString(self.theme['green']),self.thickness)) + + +class interactiveSlits(QGraphicsItemGroup): + + def __init__(self,x,y,name="NONE"): + super().__init__() + #line length will be dependent on the amount of slits + #line position will depend on the slit position of the slits (need to check slit width and postion) + #will have default lines down the middle + #default NONE next to lines that don't have a star + self.theme = get_theme() + self.setPos(x,y) + + self.bar_height = round(CSU_HEIGHT/72*MM_TO_PIXEL) #without round it = 6.06 which causes some errors + self.line = QGraphicsLineItem(x,y,x,y+self.bar_height) + self.line.setPen(QPen(QColor.fromString(self.theme['maroon']), 2)) + + self.star_name = name + self.star = QGraphicsTextItem(self.star_name) + self.star.setDefaultTextColor(QColor.fromString(self.theme['maroon'])) + self.star.setFont(QFont("Arial",6)) + self.star.setPos(x+5,y-4) + self.setFlags(self.flags() & ~self.GraphicsItemFlag.ItemIsSelectable) + + self.addToGroup(self.line) + self.addToGroup(self.star) + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + + def update_theme(self): + self.theme = get_theme() + self.line.setPen(QPen(QColor.fromString(self.theme['maroon']), 2)) + self.star.setDefaultTextColor(QColor.fromString(self.theme['maroon'])) + #have to call a paint event + def get_y_value(self): + return self.y() + def get_bar_id(self): + return int(self.y()/self.bar_height) + def get_star_name(self): + return self.star_name + +class BracketLineObject(QGraphicsItemGroup): + + def __init__(self, x_pos_of_edge_of_bar, total_height_of_bars, x_pos_of_edge_of_name, y_position_of_name,bar_height): + super().__init__() + + self.theme = get_theme() + + self.bar_pos = x_pos_of_edge_of_bar + self.height = total_height_of_bars + self.x_name_pos = x_pos_of_edge_of_name + self.y_name_pos = y_position_of_name + bar_height/2 + + self.bracket_width = 0.5 + self.padding = 0.5 + self.pen = QPen(QColor(self.theme['text'])) + self.pen.setWidth(0) + # self.pen.setStyle(Qt.PenStyle.DashLine) + multiplier = 2 + self.pen.setDashPattern([2*multiplier,1*multiplier]) + + if self.height: + self.make_bracket_and_line() + else: + self.make_line() + + + QApplication.instance().styleHints().colorSchemeChanged.connect(self.update_theme) + + def update_theme(self): + self.theme = get_theme() + self.pen = QPen(QColor(self.theme['text'])) + + def make_bracket_and_line(self): + top_edge = QGraphicsLineItem( + self.bar_pos + self.padding, + self.y_name_pos - self.height/2, + self.bar_pos + self.padding + self.bracket_width, + self.y_name_pos - self.height/2, + ) + bottom_edge = QGraphicsLineItem( + self.bar_pos + self.padding, + self.y_name_pos + self.height/2, + self.bar_pos + self.padding + self.bracket_width, + self.y_name_pos + self.height/2, + ) + bracket_edge = QGraphicsLineItem( + self.bar_pos + self.padding + self.bracket_width, + self.y_name_pos - self.height/2, + self.bar_pos + self.padding + self.bracket_width, + self.y_name_pos + self.height/2, + ) + main_line = QGraphicsLineItem( + self.bar_pos + self.padding + self.bracket_width, + self.y_name_pos, + self.x_name_pos, #maybe add padding + self.y_name_pos, + ) + + item_list = [top_edge,bottom_edge,bracket_edge,main_line] + + [item.setPen(self.pen) for item in item_list] + [self.addToGroup(item) for item in item_list] + + def make_line(self): + main_line = QGraphicsLineItem( + self.bar_pos + self.padding, + self.y_name_pos, + self.x_name_pos, + self.y_name_pos, + ) + + main_line.setPen(self.pen) + self.addToGroup(main_line) + +class CustomGraphicsView(QGraphicsView): + def __init__(self,scene): + super().__init__(scene) + # self.scene() == scene + self.previous_height = self.height() + self.previous_width = self.width() + + self.scale_x = 1.8 + self.scale_y = 1.8 #0.9 + + self.scale(self.scale_x, self.scale_y) + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + self.fitInView(scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) + self.setViewportMargins(0,0,0,0) + + def resizeEvent(self,event): + super().resizeEvent(event) + self.fitInView(self.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) + + def sizePolicy(self): + return super().sizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + def renderHints(self): + return super().renderHints(QPainter.RenderHint.Antialiasing) \ No newline at end of file diff --git a/slitmaskgui/mask_widgets/mask_view_tab_bar.py b/slitmaskgui/mask_widgets/mask_view_tab_bar.py new file mode 100644 index 0000000..d42716d --- /dev/null +++ b/slitmaskgui/mask_widgets/mask_view_tab_bar.py @@ -0,0 +1,106 @@ +# import logging +# import numpy as np +# from astroquery.gaia import Gaia +# from astropy.coordinates import SkyCoord +# import astropy.units as u +# from PyQt6.QtCore import Qt, pyqtSlot, pyqtSignal, QSize +# from PyQt6.QtGui import QBrush, QPen, QPainter, QColor, QFont, QTransform +# from slitmaskgui.mask_viewer import interactiveSlitMask, WavelengthView + +from PyQt6.QtCore import pyqtSignal, Qt, QPoint, pyqtSlot +from PyQt6.QtWidgets import ( + QTabWidget, + QComboBox, + QLabel, + QVBoxLayout, + QWidget, + QListView + +) +class CustomComboBox(QComboBox): + def __init__(self): + super().__init__() + self.spectral_view_list = [ + '[RED] Low Res Grism', + '[RED] High Res Grism (Blue End)', + '[RED] High Res Grism (Red End)', + '[BLUE] Low Res Grism', + '[BLUE] High Res Grism (Blue End)', + '[BLUE] High Res Grism (Red End)', + ] + + self.addItems(self.spectral_view_list) + + self.passbands = { #this is all in nm + "red_low_res": (550,1000), #low end, high end + "red_high_blue": (550,775), + "red_high_red": (775,1000), + "blue_low_res": (310,550), #low end, high end + "blue_high_blue": (310,435), + "blue_high_red": (430,565), + } + + def showPopup(self): + popup = self.view().window() + if popup.isVisible(): + popup.hide() + + super().showPopup() + + pos = self.mapToGlobal(QPoint(0, self.height())) + popup.move(pos) + popup.show() + + + def return_passband_from_index(self,index) -> tuple: + keys = list(self.passbands.keys()) + key = keys[index] + return self.passbands[key] + +class TabBar(QTabWidget): + waveview_change = pyqtSignal(int) + def __init__(self,slitmask_layout,waveview,skyview): + super().__init__() + #--------------defining widgets for tabs--------- + self.wavelength_view = waveview + self.interactive_slit_mask = slitmask_layout.itemAt(0).widget() + self.slit_mask = QWidget() + self.slit_mask.setLayout(slitmask_layout) + self.sky_view = skyview + + #--------------defining comobox------------------ + self.combobox = CustomComboBox() + + #--------------defining tabs-------------- + self.addTab(self.slit_mask,"Slit Mask") + self.addTab(self.wavelength_view,"Spectral View") + self.addTab(self.sky_view,"Sky View") + + self.setCornerWidget(self.combobox) + self.combobox.hide() + + #------------------connections------------ + self.tabBar().currentChanged.connect(self.wavetab_selected) + self.combobox.currentIndexChanged.connect(self.send_to_view) + + + def wavetab_selected(self,selected): + if selected == 1: #there are 3 tabs, Spectral view is the second tab so this would show combo box if spectral tab selected + self.combobox.show() + else: + self.combobox.hide() + + def send_to_view(self,index): + # I might make it so that I emit the index and a code so like Red low red is red low res red end grism as well as index + passband = self.combobox.return_passband_from_index(index) + key_list = list(self.combobox.passbands.keys()) + grism = key_list[index] + self.wavelength_view.initialize_scene(passband=passband,which_grism=grism) + + pyqtSlot(list) + def initialize_spectral_view(self, slit_positions): + index = self.combobox.currentIndex() + self.wavelength_view.get_slit_positions(slit_positions) + self.send_to_view(index) + + diff --git a/slitmaskgui/mask_widgets/sky_viewer.py b/slitmaskgui/mask_widgets/sky_viewer.py new file mode 100644 index 0000000..e5cbc83 --- /dev/null +++ b/slitmaskgui/mask_widgets/sky_viewer.py @@ -0,0 +1,81 @@ +import sys +import numpy as np +from PyQt6.QtWidgets import QApplication, QWidget, QHBoxLayout +from PyQt6.QtCore import pyqtSlot +from matplotlib.backends.backend_qtagg import FigureCanvas +from matplotlib.figure import Figure +import matplotlib.patches as patches + + +class MplCanvas(FigureCanvas): + def __init__(self, parent=None, width=5, height=4, dpi=100): + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + super().__init__(fig) + + +class SkyImageView(QWidget): + def __init__(self): + super().__init__() + + self.offline = True + + # Create the Matplotlib canvas + self.canvas = MplCanvas(self, width=5, height=4, dpi=100) + self.canvas.axes.clear() + self.canvas.axes.axis('off') + self.canvas.figure.set_facecolor('none') + + placeholder_text = "Run Mask Generation to update Skyview" + self.canvas.axes.text( + 0.5, 0.5, placeholder_text, + horizontalalignment='center', + verticalalignment='center', + transform=self.canvas.axes.transAxes, + fontsize=14, + color='grey' + ) + self.canvas.draw() + + + layout = QHBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(1,0,1,0) + layout.addWidget(self.canvas) + + self.setLayout(layout) + self.resize(self.sizeHint()) + + def update_image(self,slitmask): + if not self.offline: + self.show_image(slitmask.generate_skyview()) + else: + print("No skyview due to being offline") + + pyqtSlot(np.ndarray) + def show_image(self, data: np.ndarray): + + # Clear previous plot + self.canvas.axes.clear() + self.canvas.axes.imshow(data, origin='lower', cmap='gray') + # self.canvas.axes.set_title("Sky Image (DSS2 Red)") + + self.canvas.axes.axis('off') + #The image is 900 px by 1000 px which I have no clue if that is good but it is what it is right now + fig_size_inches = self.canvas.figure.get_size_inches() + dpi = self.canvas.figure.dpi + + width_pixels = fig_size_inches[0] * dpi + height_pixels = fig_size_inches[1] * dpi + + rect = patches.Rectangle( + (900/4, 0), # bottom-left corner (x, y) + 900/2, 1000, # width, height + linewidth=4, + edgecolor='green', + facecolor='none', + alpha=0.4 # transparency + ) + self.canvas.axes.add_patch(rect) + self.canvas.figure.tight_layout(pad=0) + self.canvas.draw() diff --git a/slitmaskgui/mask_widgets/slitmask_view.py b/slitmaskgui/mask_widgets/slitmask_view.py new file mode 100644 index 0000000..65e5250 --- /dev/null +++ b/slitmaskgui/mask_widgets/slitmask_view.py @@ -0,0 +1,222 @@ +""" +This is the interactive slit mask feature. It will interact with the bar table on the left. +when you click the bar on the left then the image will display which row that is +additionally It will also interact with the target list +it will display where the slit is place and what stars will be shown +""" + +from slitmaskgui.mask_widgets.mask_objects import * +from itertools import groupby +import logging +import numpy as np +from PyQt6.QtCore import Qt, pyqtSlot, pyqtSignal, QSize +from PyQt6.QtWidgets import ( + QVBoxLayout, + QHBoxLayout, + QWidget, + QLabel, + QGraphicsScene, + QGraphicsRectItem, + QGraphicsItemGroup, +) + +#will have another thing that will dispaly all the stars in the sky at the time +PLATE_SCALE = 0.7272 #(mm/arcsecond) on the sky +CSU_HEIGHT = PLATE_SCALE*60*10 #height of csu in mm (height is 10 arcmin) +CSU_WIDTH = PLATE_SCALE*60*5 #width of the csu in mm (widgth is 5 arcmin) +MM_TO_PIXEL = 1 #this is a mm to pixel ratio, it is currently just made up + +logger = logging.getLogger(__name__) + + +class interactiveSlitMask(QWidget): + """ interactive slit mask is a display widget + with the ability to update objects in its display + depending on outside signals """ + + + row_selected = pyqtSignal(int,name="row selected") + select_star = pyqtSignal(str) + new_slit_positions = pyqtSignal(list) + + def __init__(self): + super().__init__() + + #--------------------definitions----------------------- + logger.info("slit_view: doing definitions") + self.scene_width = (CSU_WIDTH+CSU_WIDTH/1.25) * MM_TO_PIXEL + scene_height = CSU_HEIGHT * MM_TO_PIXEL + self.scene = QGraphicsScene(0,0,self.scene_width,scene_height) + + xcenter_of_image = self.scene.sceneRect().center().x() + + self.mask_name_title = QLabel(f'MASK NAME: None') + self.center_title = QLabel(f'CENTER: None') + self.pa_title = QLabel(f'PA: None') + + self.all_bars = [] + self.all_slits = [] + + bar_length = self.scene_width + self.bar_height = CSU_HEIGHT/72#PLATE_SCALE*8.6 + padding = 0 + + for i in range(72): + temp_rect = interactiveBars(0,i*self.bar_height+padding,this_id=i,bar_width=self.bar_height,bar_length=bar_length) + temp_slit = interactiveSlits(self.scene_width/2,self.bar_height*i+padding) + self.scene.addItem(temp_rect) + self.scene.addItem(temp_slit) + + self.update_list_of_all_bars_in_scene() + self.update_list_of_all_slits_in_scene() + + fov = FieldOfView(x=xcenter_of_image/2,y=padding) + new_center = fov.boundingRect().center().x() + new_x = xcenter_of_image-new_center + fov.setPos(new_x,0) + self.scene.addItem(fov) + + self.scene.setSceneRect(self.scene.itemsBoundingRect()) + self.view = CustomGraphicsView(self.scene) + self.view.setContentsMargins(0,0,0,0) + + #-------------------connections----------------------- + logger.info("slit_view: establishing connections") + self.connect_signalers() + + #------------------------layout----------------------- + logger.info("slit_view: defining layout") + top_layout = QHBoxLayout() + main_layout = QVBoxLayout() + + top_layout.addWidget(self.mask_name_title,alignment=Qt.AlignmentFlag.AlignHCenter) + top_layout.addWidget(self.center_title,alignment=Qt.AlignmentFlag.AlignHCenter) + top_layout.addWidget(self.pa_title,alignment=Qt.AlignmentFlag.AlignHCenter) + main_layout.addLayout(top_layout) + main_layout.setSpacing(0) + main_layout.setContentsMargins(0,0,0,0) + main_layout.addWidget(self.view) + + self.setLayout(main_layout) + #------------------------------------------- + def sizeHint(self): + return QSize(550,620) + + def connect_signalers(self): + self.scene.selectionChanged.connect(self.row_is_selected) + self.scene.selectionChanged.connect(self.get_star_name_from_row) + + def disconnect_signalers(self): + try: + self.scene.selectionChanged.disconnect(self.row_is_selected) + self.scene.selectionChanged.disconnect(self.get_star_name_from_row) + except: + logger.info(f'Slitmask View: \'disconnect_signalers\' failed') + + @pyqtSlot(int,name="row selected") + def select_corresponding_row(self,row): + logger.info("slit_view: method select_correspond_row called") + self.scene.clearSelection() + if 0 <= row len(self.all_slits): + self.add_forgotten_slits(pos,x_center) + + self.update_list_of_all_slits_in_scene() + self.emit_slit_positions(self.all_slits) + + def update_existing_slits(self,index,slit,position,x_center): + x_pos, *name = position[index] + slit_position, star_name = (x_center+x_pos, index*self.bar_height), " ".join(name).strip() + slit.setPos(slit_position[0],slit_position[1]) + slit.star.setPlainText(star_name) + + def add_forgotten_slits(self,positions,x_center): + already_added_id_list = [slit.get_bar_id() for slit in self.all_slits] + unadded_list = set(positions.keys()) ^ set(already_added_id_list) + + for unadded in unadded_list: + x_pos, *name = positions[unadded] + unadded_slit = interactiveSlits(x_center+x_pos, unadded*self.bar_height, " ".join(name).strip()) + self.scene.addItem(unadded_slit) + + @pyqtSlot(np.ndarray, name="update labels") + def update_name_center_pa(self,info): + mask_name, center, pa = info[0], info[1], info[2] #the format of info is [mask_name,center,pa] + if type(center) is tuple(): + center = str(center[0])+str(center[1]) + self.mask_name_title.setText(f'MASK NAME: {mask_name}') + self.center_title.setText(f'CENTER: {center}') + self.pa_title.setText(f'PA: {pa}') + + def emit_slit_positions(self,slits): + slit_positions = [(slit.x(),slit.y(),slit.star_name) for slit in slits] #-(x_center-x.xpos) gets distance from center where left is negative + self.new_slit_positions.emit(slit_positions) + + def update_list_of_all_bars_in_scene(self): + self.all_bars = [ + item for item in reversed(self.scene.items()) + if isinstance(item, interactiveBars) + ] + + def update_list_of_all_slits_in_scene(self): + self.all_slits = [ + item for item in reversed(self.scene.items()) + if isinstance(item, interactiveSlits) + ] + + + + + +# Define the coordinates (RA, Dec) - replace with your values + diff --git a/slitmaskgui/mask_widgets/waveband_view.py b/slitmaskgui/mask_widgets/waveband_view.py new file mode 100644 index 0000000..9ccc0d4 --- /dev/null +++ b/slitmaskgui/mask_widgets/waveband_view.py @@ -0,0 +1,313 @@ + +from slitmaskgui.mask_widgets.mask_objects import * +from itertools import groupby +import logging +from PyQt6.QtCore import Qt, pyqtSlot, pyqtSignal, QSize +from PyQt6.QtGui import QPen, QColor, QFont +from PyQt6.QtWidgets import ( + QVBoxLayout, + QWidget, + QGraphicsScene, + QGraphicsRectItem, + QGraphicsTextItem, + QHBoxLayout, + QLabel, + QGraphicsSceneResizeEvent +) + +#will have another thing that will dispaly all the stars in the sky at the time +PLATE_SCALE = 0.7272 #(mm/arcsecond) on the sky +CSU_HEIGHT = PLATE_SCALE*60*10 #height of csu in mm (height is 10 arcmin) +CSU_WIDTH = PLATE_SCALE*60*5 #width of the csu in mm (widgth is 5 arcmin) +MM_TO_PIXEL = 1 #this is a mm to pixel ratio, it is currently just made up +MAGNIFICATION_FACTOR = 7.35 +CCD_HEIGHT = 61.2 #in mm +CCD_WIDTH = 61.2 #in mm + + +logger = logging.getLogger(__name__) + + + +""" +red and blue and 3 grisms for each + +currently have the center of the bar move to the place where the slit is, and +""" +class WavelengthView(QWidget): + row_selected = pyqtSignal(int,name="row selected") + + def __init__(self): + super().__init__() + + #--------------------definitions----------------------- + logger.info("wave view: doing definitions") + self.scene_width = CCD_WIDTH* MM_TO_PIXEL + self.scene_height = CCD_HEIGHT * MM_TO_PIXEL + + self.scene = QGraphicsScene(0,0,self.scene_width,self.scene_height) + + #since this is being fed information from CSU, it automatically adjusts from CSU positions + #so initialize as CSU and it will change it + #this is mostly for testing + self.CSU_dimensions = (CSU_HEIGHT,CSU_WIDTH) + xcenter_of_image = self.scene.sceneRect().center().x() + + self.mask_name = None + self.bar_height = CCD_HEIGHT/72#PLATE_SCALE*8.6 #this could be wrong maybe use magnification factor + + self.passband_title = QLabel() + + self.slit_positions = [(xcenter_of_image,self.bar_height*x, "NONE") for x in range(72)] + self.initialize_scene(passband=(310,550),which_grism='blue_low_res') # passband currently a temp variable + self.view = CustomGraphicsView(self.scene) + self.view.setContentsMargins(0,0,0,0) + + #-------------------connections----------------------- + logger.info("wave view: establishing connections") + self.connect_signalers() + + #------------------------layout----------------------- + logger.info("wave view: defining layout") + + main_layout = QVBoxLayout() + top_layout = QHBoxLayout() + + self.passband_title.setAlignment(Qt.AlignmentFlag.AlignHCenter) + top_layout.addWidget(self.passband_title) + + main_layout.addLayout(top_layout) + main_layout.setSpacing(0) + main_layout.setContentsMargins(0,0,0,0) + main_layout.addWidget(self.view) + + self.setLayout(main_layout) + #------------------------------------------- + def sizeHint(self): + return QSize(650,620) + + def connect_signalers(self): + self.scene.selectionChanged.connect(self.send_row) + + def disconnect_signalers(self): + try: + self.scene.selectionChanged.disconnect(self.send_row) + except: + print("disconnect for waveband view failed") + + @pyqtSlot(int,name="row selected") + def select_corresponding_row(self,row): + all_bars = [ + item for item in reversed(self.scene.items()) + if isinstance(item, QGraphicsRectItem) + ] + self.disconnect_signalers() + self.scene.clearSelection() + if 0 <= row QGraphicsRectItem: + + x_position = x_pos - length/2 + new_bar = interactiveBars(x_position,y_pos,this_id=star_name,bar_width=self.bar_height,bar_length=length,has_gradient=True) + + return new_bar + + def concatenate_stars(self, slit_positions) -> list: + """ + Concatenates the positions of the star_name text + + Args: + slit_positions: the positions of the slits on the slitmask (also contains the name of the star) + Returns: + List of all the names that will be displayed and the y_position of those names + """ + star_name_positions = [sublist[1:] for sublist in slit_positions] + star_name_positions.sort(key=lambda x:x[1]) + name_positions = [] + for name, group in groupby(star_name_positions, key=lambda x: x[1]): + group = list(group) + max_y_pos = max(group, key=lambda x: x[0])[0] + min_y_pos = min(group, key=lambda x: x[0])[0] + average_y_pos = (max_y_pos+min_y_pos)/2 + name_positions.append((average_y_pos,name)) + return name_positions + + def make_star_text(self,x_pos, y_pos, text): + """ Makes a text item given an x_position, y_position, and the text to be displayed """ + + text_item = SimpleTextItem(text) + offset = (text_item.boundingRect().width()/2,text_item.boundingRect().height()/2) + text_item.setPos(x_pos-offset[0]/2,y_pos-offset[1]+self.bar_height) + return text_item + + def find_edge_of_bar(self,bar_items)-> list: + """ + groups bars of same length and x position into a list containing info on + which star the bars correspond to, the right edge of the bar, and the total height of the bars + + Args: + bar_items: list of all the bars + Returns: + list of information formatted like [(right edge of bar, height, name of star),...] + IMPORTANT: if there is only one bar, total_height_of_bars will = 0 + """ + + new_list = sorted( + [[bar.x_pos + bar.length, bar.y_pos, bar.id] for bar in bar_items], + key=lambda x: x[2] + ) + + new_bar_list = [] + for name, group in groupby(new_list, key=lambda x: x[2]): + group = [sublist[:-1] for sublist in list(group)] + max_y_pos = max(group, key=lambda x: x[1])[1] + min_y_pos = min(group, key=lambda x: x[1])[1] + total_height_of_bars = max_y_pos-min_y_pos + new_bar_list.append((group[0][0],total_height_of_bars,name)) + + return new_bar_list + + def make_line_between_text_and_bar(self, bar_positions, name_positions,edge_of_name) -> list: + """ + makes a line with a bracket that connects the star names on the right side with + the corresponding passbands in the middle + + Args: + bar_positions: positions of passbands formatted like [(x_bar,height,star_name),...] + name_positions: positions of the start names formatted like [(y_pos,name),...] + Returns: + List of all the bracket line objects + """ + bars, names, name_edge = bar_positions, name_positions, edge_of_name + sorted_merged_list = sorted(bars + names, key=lambda x: x[-1]) + + information_list = [] + object_list = [] + + for name, group in groupby(sorted_merged_list,key=lambda x: x[-1]): + group = [sublist[:-1] for sublist in list(group)] + # group = [(x_bar,height),(name_y_pos,)] + information_list.append([group[0][0],group[0][1],name_edge,group[1][0]]) + # information_list = [[a=x_bar,b=height,c=name_edge,d=y_pos]] + [object_list.append(BracketLineObject(a,b,c,d,bar_height=self.bar_height)) for a,b,c,d in information_list] + return object_list + + def update_angstrom_text(self,passband): + """ Updates the text of the passband to ensure it is accurate with the desired display """ + text = f"Passband: {passband[0]} nm to {passband[1]} nm" + self.passband_title.setText(text) + + def calculate_bar_length(self,passband,which_grism): + """ + calculates how long the passband will be for the selected grism + + Args: + passband: the range of the passband in nm + which_grism: a string of the selected grism + Returns: + length of the bar depending on selected grism + """ + + x_low, x_high = passband[0]/1000, passband[1]/1000 #conversion from nm to microns + + def compute_passband_endpoints(a: float, b: float, c: float, d: float, x: float) -> float: + return a * x**3 + b * x**2 + c * x + d + + coefficients = { + "blue_low_res": (276.612, -424.636, 413.464, -120.251), + "blue_high_blue": (1694.055, -2185.377, 1398.040, -303.935), + "blue_high_red": (791.523, -1338.208, 1171.084, -348.142), + "red_low_res": (21.979, -60.775, 183.657, -115.552), + "red_high_blue": (117.366, -273.597, 461.561, -219.310), + "red_high_red": (76.897, -235.837, 479.807, -292.794), + } + + a, b, c, d = coefficients[which_grism] + low_end = compute_passband_endpoints(a, b, c, d, x_low) + high_end = compute_passband_endpoints(a, b, c, d, x_high) + return max(0.0, high_end - low_end) + + def get_farthest_bar_edge(self,scene): + """ Locates the bar furthest to the right and returns the right edge x position of that bar """ + bar_edge_list = [ + bar.boundingRect().right() + for bar in scene.items() + if isinstance(bar, interactiveBars) + ] + farther_edge = max(bar_edge_list) + return farther_edge + 1 # number is 0.5 + 0.5 from the bracket item in mask_objects (spacing and bracket width) + + + def redefine_slit_positions(self,slit_positions): + """ Converts the slit positions that were defined for the CSU + into the corresponding position on the CCD """ + y_ratio = self.CSU_dimensions[0]/CCD_HEIGHT + new_pos = [(x/MAGNIFICATION_FACTOR,y/y_ratio, name) for x,y,name in slit_positions] + return new_pos + + + def initialize_scene(self, passband, which_grism): + """ + initializes scene of selected grism + assumes index corresponds to Red low, red high blue, red high red, blue low, blue high blue, blue high red + + Args: + index: the index of what box was selected (corresponds with the grism) + Kwargs: + which_grism: name of the grism + passband: wavelength range that will be covered + returns: + None + """ + + new_scene = self.scene + [new_scene.removeItem(item) for item in new_scene.items()] #removes all items + + passband_in_nm = passband + grism = which_grism + bar_length = self.calculate_bar_length(passband_in_nm,grism) + + # ADD all the bars with slits + [new_scene.addItem(self.make_new_bar(x,y,name,length=bar_length)) for x,y,name in self.slit_positions] + + # Add a rectangle representing the CCD camera FOV (is currently not accurate) + camera_border = FieldOfView(width=CCD_WIDTH*MM_TO_PIXEL,height=CCD_HEIGHT*MM_TO_PIXEL,x=0,y=0, thickness=0.5) + new_scene.addItem(camera_border) + + # Add all the names of the stars on the side + rightmost_bar_x = self.get_farthest_bar_edge(new_scene) + name_positions = self.concatenate_stars(self.slit_positions) + [new_scene.addItem(self.make_star_text(rightmost_bar_x,y,text)) for y,text in name_positions] + + # Add lines and brackets to point from star name to bar + all_bar_objects = [bar for bar in new_scene.items() if isinstance(bar, interactiveBars)] + edge_of_bar_list = self.find_edge_of_bar(all_bar_objects) + bracket_list = self.make_line_between_text_and_bar(edge_of_bar_list,name_positions,rightmost_bar_x) + [new_scene.addItem(item) for item in bracket_list] + + # Update passband text + self.update_angstrom_text(passband_in_nm) #it is no longer the angstrom range + + new_scene.setSceneRect(new_scene.itemsBoundingRect()) + self.scene = new_scene + + + def update_mask_name(self,info): + self.mask_name = info[0] + print(self.mask_name) + \ No newline at end of file diff --git a/slitmaskgui/offline_mode.py b/slitmaskgui/offline_mode.py new file mode 100644 index 0000000..2e43ba8 --- /dev/null +++ b/slitmaskgui/offline_mode.py @@ -0,0 +1,133 @@ +from slitmaskgui.mask_widgets.mask_objects import * +from itertools import groupby +import logging +import numpy as np +import time +import requests +import socket +from PyQt6.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, QObject, pyqtSignal, QTimer +from PyQt6.QtWidgets import ( + QVBoxLayout, + QHBoxLayout, + QWidget, + QLabel, +) + + + + +HOST = '131.215.200.105' +PORT = 5571 + + +""" +Initially I will have this just check if you are offline or not. +But in the future I might have it check if you are also connected to the CSU using the socket module +""" + +class OfflineCheckerSignals(QObject): + started = pyqtSignal() + finished = pyqtSignal() + error = pyqtSignal(tuple) + connection_status = pyqtSignal(object) + + +class CSUConnectionSignals(QObject): + started = pyqtSignal() + finished = pyqtSignal() + error = pyqtSignal(tuple) + connection_status = pyqtSignal(bool) + + +class InternetConnectionChecker(QRunnable): + """ class that constantly checks if the user is online or offline """ + + def __init__(self): + super().__init__() + self.signals = OfflineCheckerSignals() + + @pyqtSlot() + def run(self): + """ the online and having to do not online is needlessly confusing I think """ + self.signals.started.emit() + online = self.check_internet_connection() + self.signals.connection_status.emit(not online) # return not online because we are seeing if it is offline + self.signals.finished.emit() + + def check_internet_connection(self): + """ I feel like this is not a good way to do this """ + try: + response = requests.get("https://www.google.com/", timeout=5) + return True + except requests.ConnectionError: + return False + + +class CSUConnectionChecker(QRunnable): + """ Checks the connection to the CSU """ + def __init__(self): + super().__init__() + self.signals = CSUConnectionSignals() + + @pyqtSlot() + def run(self): + self.signals.started.emit() + csu_connection_status = self.check_connected_to_CSU() + self.signals.connection_status.emit(csu_connection_status) + self.signals.finished.emit() + + def check_connected_to_CSU(self): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(5) + try: + sock.connect((HOST,PORT)) + return True + except socket.error: + return False + + +class OfflineMode(QObject): + + current_mode = pyqtSignal(object) + + def __init__(self): + super().__init__() + self.offline = False + + self.threadpool = QThreadPool() + self.offline_checker = InternetConnectionChecker() + + # ------------- timer ---------------- + self.timer = QTimer() + self.timer.setInterval(1000) + self.timer.timeout.connect(self.start_checking_internet_connection) # having what the timer is connected to not be known by the user is a bit confusing + # ------------------------------------ + + def __repr__(self): + if self.offline: + return f'Offline' + return f'Online' + + def start_checking_internet_connection(self): # this feels kind of bad but its fine for now + self.offline_checker.signals.connection_status.connect(self.change_mode) + self.threadpool.start(self.offline_checker) + self.offline_checker = InternetConnectionChecker() + + def start_timer(self): + self.timer.start() + + def stop_timer(self): + self.timer.stop() + + def change_mode(self,mode): + self.offline = mode + self.current_mode.emit(self.offline) + + + + + + # def check_csu_connection(self): # Offline mode will soon not be able to check if csu is connected or not + # self.threadpool.connect_internet_checker_signals(self.change_mode) + # self.threadpool.start_csu_connection_checker() + \ No newline at end of file diff --git a/slitmaskgui/requirements.txt b/slitmaskgui/requirements.txt new file mode 100644 index 0000000..48cc071 --- /dev/null +++ b/slitmaskgui/requirements.txt @@ -0,0 +1,49 @@ +astropy==7.1.0 +astropy-iers-data==0.2025.7.14.0.40.29 +astroquery==0.4.10 +beautifulsoup4==4.13.4 +certifi==2025.7.14 +charset-normalizer==3.4.2 +contourpy==1.3.3 +-e git+https://github.com/CaltechOpticalObservatories/coo-ethercat.git@e80ca28422081dd73c21de01f0ce2b6182eebdc9#egg=cooethercat +cycler==0.12.1 +fonttools==4.59.0 +html5lib==1.1 +idna==3.10 +iniconfig==2.1.0 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.2.1 +keyring==25.6.0 +kiwisolver==1.4.9 +-e git+https://github.com/CaltechOpticalObservatories/lris2-csu.git@b929447eae6185f5bda6dbbeea7d3864d4380231#egg=lris2csu +matplotlib==3.10.5 +-e git+https://github.com/baileyji/mKTL.git@4aef3d3fd60e9a86f07656a712d6c81432729781#egg=mKTL +more-itertools==10.7.0 +numpy==2.3.1 +packaging==25.0 +pandas==2.3.1 +pillow==11.3.0 +pluggy==1.6.0 +pyerfa==2.0.1.5 +Pygments==2.19.2 +pyparsing==3.2.3 +PyQt6==6.9.1 +PyQt6-Qt6==6.9.1 +PyQt6_sip==13.10.2 +pysoem==1.1.12 +pytest==8.4.1 +pytest-qt==4.5.0 +python-dateutil==2.9.0.post0 +pytz==2025.2 +pyvo==1.7 +PyYAML==6.0.2 +pyzmq==27.0.1 +requests==2.32.4 +six==1.17.0 +soupsieve==2.7 +typing_extensions==4.14.1 +tzdata==2025.2 +urllib3==2.5.0 +webencodings==0.5.1 +zmq==0.0.0 diff --git a/slitmaskgui/send_to_csu.py b/slitmaskgui/send_to_csu.py deleted file mode 100644 index 4fd3d78..0000000 --- a/slitmaskgui/send_to_csu.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -This function will instruct the csu -""" \ No newline at end of file diff --git a/slitmaskgui/slit_position_table.py b/slitmaskgui/slit_position_table.py index 8c4db51..dba6121 100644 --- a/slitmaskgui/slit_position_table.py +++ b/slitmaskgui/slit_position_table.py @@ -6,18 +6,15 @@ from slitmaskgui.menu_bar import MenuBar import logging -import itertools -from PyQt6.QtCore import Qt, QAbstractTableModel, pyqtSlot, pyqtSignal, QSize +from itertools import groupby +from PyQt6.QtCore import Qt, QAbstractTableModel, pyqtSignal, QSize from PyQt6.QtWidgets import ( QWidget, QTableView, QVBoxLayout, - QTableWidget, QSizePolicy, - QLabel, QHeaderView, - QFrame, - QAbstractScrollArea + QAbstractScrollArea, ) @@ -31,11 +28,9 @@ def __init__(self, data=[]): self.headers = ["Row","Center","Width"] def headerData(self, section, orientation, role = ...): if role == Qt.ItemDataRole.DisplayRole: - #should add something about whether its vertical or horizontal if orientation == Qt.Orientation.Horizontal: return self.headers[section] if orientation == Qt.Orientation.Vertical: - return None return super().headerData(section, orientation, role) @@ -51,7 +46,7 @@ def data(self, index, role): def rowCount(self, index): return len(self._data) - + def columnCount(self, index): try: return len(self._data[0]) @@ -71,26 +66,23 @@ def flags(self, index): def setData(self, index, value, role = ...): if role == Qt.ItemDataRole.EditRole: - # Set the value into the frame. self._data[index.row()][index.column()] = value - self.dataChanged.emit(index, index) - return True + self.dataChanged.emit(index,index) + return True return False - # return super().setData(index, value, role) + class CustomTableView(QTableView): + data_changed = pyqtSignal(object,object) def __init__(self): super().__init__() - self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(0) - self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) self.setSelectionMode(QTableView.SelectionMode.SingleSelection) - - self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) + def setModel(self, model): super().setModel(model) self.setResizeMode() @@ -99,14 +91,8 @@ def setResizeMode(self): self.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) - - def event(self, event): - return super().event(event) - #what I will do in the future is make it so that if even == doublemousepress event that you can edit the data in the cell - - width = .7 default_slit_display_list = [[i+1,0.00,width] for i in range(72)] @@ -114,8 +100,8 @@ def event(self, event): class SlitDisplay(QWidget): highlight_other = pyqtSignal(int,name="row selected") #change name to match that in the interactive slit mask select_star = pyqtSignal(int) - data_changed = pyqtSignal(list) #it will have a bool as the first part of the list - tell_unsaved = pyqtSignal(object) + data_changed = pyqtSignal(dict) #it will have a bool as the first part of the list + tell_unsaved = pyqtSignal() def __init__(self,data=default_slit_display_list): super().__init__() @@ -130,19 +116,16 @@ def __init__(self,data=default_slit_display_list): self.table = CustomTableView() self.model = TableModel(self.data) self.table.setModel(self.model) - self.changed_data_list = [False,{}] + self.changed_data_dict = {} #--------------------------connections----------------------- logger.info("slit_position_table: doing conections") - self.table.selectionModel().selectionChanged.connect(self.row_selected) - self.model.dataChanged.connect(self.slit_width_changed) - + self.connect_signalers() #----------------------------layout---------------------- logger.info("slit_position_table: defining layout") main_layout = QVBoxLayout() - # main_layout.setSpacing(9) main_layout.setContentsMargins(0,0,9,0) main_layout.addWidget(self.table) @@ -151,19 +134,20 @@ def __init__(self,data=default_slit_display_list): def sizeHint(self): return QSize(170,120) - def connect_on(self,answer:bool): - #---------------reconnect connections--------------- - if answer: - self.table.selectionModel().selectionChanged.connect(self.row_selected) - else: - self.table.selectionModel().selectionChanged.disconnect(self.row_selected) - @pyqtSlot(list,name="input slit positions") + def connect_signalers(self): + self.table.selectionModel().selectionChanged.connect(self.row_selected) + self.model.dataChanged.connect(self.slit_width_changed) + + def disconnect_signalers(self): + self.table.selectionModel().selectionChanged.disconnect(self.row_selected) + self.model.dataChanged.disconnect(self.slit_width_changed) + def change_data(self,data): logger.info("slit_position_table: change_data function called, changing data") if data: self.model.beginResetModel() - replacement = list(x for x,_ in itertools.groupby(data)) + replacement = list(x for x,_ in groupby(data)) self.model._data = replacement self.data = replacement self.model.endResetModel() @@ -175,14 +159,11 @@ def row_selected(self): logger.info("slit_position_table: method row_selected is called, row in slit_table was selected") selected_row = self.table.selectionModel().currentIndex().row() corresponding_row = self.model.get_bar_id(row=selected_row) - self.highlight_other.emit(corresponding_row-1) - - @pyqtSlot(int,name="other row selected") def select_corresponding(self,bar_id): logger.info("slit_position_table: method select_corresponding is called, selected corresponding row from slit mask view") - self.connect_on(False) + self.disconnect_signalers() self.bar_id = bar_id + 1 filtered_row = list(filter(lambda x:x[0] == self.bar_id,self.data)) @@ -193,28 +174,18 @@ def select_corresponding(self,bar_id): else: #this means that the bar does not have a slit on it pass - self.connect_on(True) - - def slit_width_changed(self,index1,index2): - #essentially, if the data was changed, this would be set to true and the rest of the program would know the data is changed - #so the config would set the state to be unsaved - #once the mask config has been saved everything else will update - #it will also say what has been changed - index_1, index_2 = index1, index2 - self.changed_data_list[0]=True - - #if index_1 and index_2 do not equal each other then multiple lines of data have been changed - #I won't worry about that right now - if index_1 == index_2: - value = self.model.data(index_2,Qt.ItemDataRole.DisplayRole) - bar_id = self.model.get_bar_id(index_2.row()) - self.changed_data_list[1][bar_id]=value - else: - pass #will add all the changed data in a loop - self.tell_unsaved.emit(None) #doesn't have to be a bool or really anything just need to signal - pyqtSlot(object) + self.connect_signalers() + + def slit_width_changed(self,topLeft,bottomRight): + row = topLeft.row() + model = topLeft.model() + new_data = model.data(topLeft, Qt.ItemDataRole.DisplayRole) + bar_id = model.get_bar_id(row) -1 + self.changed_data_dict[bar_id]=new_data + + self.tell_unsaved.emit() + def data_saved(self): - list_copy = self.changed_data_list - self.changed_data_list = [False,{}] - self.data_changed.emit(list_copy) + self.data_changed.emit(self.changed_data_dict) + self.changed_data_dict = {} diff --git a/slitmaskgui/target_list_widget.py b/slitmaskgui/target_list_widget.py index 7ac4dd1..db394b9 100644 --- a/slitmaskgui/target_list_widget.py +++ b/slitmaskgui/target_list_widget.py @@ -1,14 +1,12 @@ #from inputTargets import TargetList import logging -from slitmaskgui.menu_bar import MenuBar from PyQt6.QtCore import Qt, QAbstractTableModel, pyqtSlot, QSize, pyqtSignal import itertools from PyQt6.QtWidgets import ( QWidget, QTableView, QVBoxLayout, - QLabel, QSizePolicy, QHeaderView, @@ -109,9 +107,9 @@ def __init__(self,data=[]): def sizeHint(self): return QSize(700,100) - def connect_on(self,answer:bool): + def toggle_connection(self,connect:bool): #---------------reconnect connections--------------- - if answer: + if connect: self.table.selectionModel().selectionChanged.connect(self.selected_star) else: self.table.selectionModel().selectionChanged.disconnect(self.selected_star) @@ -134,12 +132,12 @@ def selected_star(self): @pyqtSlot(str) def select_corresponding(self,star): #everything will be done with the row widget - self.connect_on(False) + self.toggle_connection(False) row = self.model.get_row(star) if row in range(len(self.model._data)): logger.info(f'target_list_widget: method select_corresponding called: row {row}') self.table.selectRow(row) - self.connect_on(True) + self.toggle_connection(True) diff --git a/slitmaskgui/tests/test_input_targets.py b/slitmaskgui/tests/test_input_targets.py index 7f8c0ba..2d2c3ae 100644 --- a/slitmaskgui/tests/test_input_targets.py +++ b/slitmaskgui/tests/test_input_targets.py @@ -1,22 +1,14 @@ -from slitmaskgui.input_targets import TargetList +from slitmaskgui.backend.input_targets import TargetList import pytest +import json -#third one should return an error, fourth one shouldn't return error but must_have wont be read -#add something to input_targets that makes it so that if one thing fails it doesn't all fail +@pytest.fixture +def sample_target_list(): + with open('slitmaskgui/tests/testfiles/gaia_target_list.json','r') as file: + return json.load(file) -#I just have to test the parsing - - -def test_parsing(): - target_list = TargetList("slitmaskgui/tests/testfiles/star_list.txt") +def test_parsing(sample_target_list): + target_list = TargetList("slitmaskgui/tests/testfiles/gaia_starlist.txt") object = target_list.send_json() - for x,index in enumerate(object): - if index == 0: - assert x == {"name": "Gaia_001", "ra": "15 25 32.35", "dec": "-50 46 46.8","equinox": "2000.0","vmag": "20.77","priority": "1020"} - if index == 1: - assert x == {"name": "Gaia_001", "ra": "15 25 32.35", "dec": "-50 46 46.8","equinox": "2000.0","vmag": "20.77","priority": "1020"} - if index == 2: - assert x == {"name": "UntitledStar0", "ra": "Not Provided", "dec": "Not Provided","equinox": "Not Provided","vmag": "20.77","priority": "1020"} - if index == 3: - assert x == {"name": "Gaia_001", "ra": "15 25 32.35", "dec": "-50 46 46.8","equinox": "2000.0","vmag": "20.77","priority": "1020"} - + for star_item in json.loads(object): + assert star_item in sample_target_list diff --git a/slitmaskgui/tests/test_mask_configurations.py b/slitmaskgui/tests/test_mask_configurations.py new file mode 100644 index 0000000..4bb4a50 --- /dev/null +++ b/slitmaskgui/tests/test_mask_configurations.py @@ -0,0 +1,70 @@ +import pytest +from slitmaskgui.mask_configurations import MaskConfigurationsWidget, CustomTableView, TableModel +from slitmaskgui.slit_position_table import SlitDisplay +import json +from unittest.mock import patch, Mock +import unittest +from pytest import MonkeyPatch + +""" To test the connections between the classes we will test app.py""" + +MASK_NAME_1 = "name" +MASK_NAME_2 = "other_name" + + +@pytest.fixture +def sample_config_data(): + with open('slitmaskgui/tests/testfiles/gaia_mask_config.json','r') as file: + return json.load(file) + +@pytest.fixture +def setup_mask_config_class(qtbot): + config = MaskConfigurationsWidget() + qtbot.addWidget(config) + return config + + +def initialize_configuration(test_mask_config, sample_config_data): + with patch.object(test_mask_config.model, 'beginResetModel'), \ + patch.object(test_mask_config.model, 'endResetModel'), \ + patch.object(test_mask_config.table, 'selectRow'): + test_mask_config.initialize_configuration((MASK_NAME_1, sample_config_data)) + return test_mask_config + + +def test_initialize_configuration(setup_mask_config_class, sample_config_data): + test_mask_config = initialize_configuration(setup_mask_config_class, sample_config_data) + assert test_mask_config.row_to_config_dict == {0: sample_config_data} + assert test_mask_config.model._data == [["Saved", MASK_NAME_1]] + + +def test_clicking_save_button_emits_signal(setup_mask_config_class, qtbot): + test_mask_config = setup_mask_config_class + with qtbot.waitSignal(test_mask_config.data_to_save_request) as save_button_clicked: + test_mask_config.save_button.click() + assert True # passed without timeout + + +def test_update_table_to_saved(setup_mask_config_class): + test_mask_config = setup_mask_config_class + test_mask_config.update_table_to_saved(0) + assert test_mask_config.model._data[0] == ["Saved", MASK_NAME_1] + + +# def test_switching_masks() + + +# def test_export_button() + + +# def test_export_all_button() + + +# def test_close_button() + + +# def test_open_button() + + + + diff --git a/slitmaskgui/tests/test_mask_gen.py b/slitmaskgui/tests/test_mask_gen.py deleted file mode 100644 index e2c4e39..0000000 --- a/slitmaskgui/tests/test_mask_gen.py +++ /dev/null @@ -1,54 +0,0 @@ -# from slitmaskgui.backend.mask_gen import SlitMask - - -import pytest -from slitmaskgui.backend.mask_gen import SlitMask, CSU_HEIGHT, CSU_WIDTH, TOTAL_BAR_PAIRS -""" -will be in a list of obj {name,ra,dec,equinox,vmag,priority,bar_id,x_mm,y_mm} -""" - - -# print(stars) - - -def test_check_if_within_bounds(): - sm = SlitMask([]) - - assert sm.check_if_within(0, 0) is True - assert sm.check_if_within(CSU_WIDTH/2 - 0.01, CSU_HEIGHT/2 - 0.01) is True - assert sm.check_if_within(CSU_WIDTH/2 + 1, 0) is False - assert sm.check_if_within(0, CSU_HEIGHT/2 + 1) is False - -def test_bar_id_assignment_center(): - stars = [{"x_mm": 0, "y_mm": 0, "priority": 1}] - mask = SlitMask(stars) - assert "bar_id" in mask.stars[0] - assert mask.stars[0]["bar_id"] == TOTAL_BAR_PAIRS // 2 - -def test_out_of_bounds_star_is_removed(): - stars = [ - {"x_mm": 0, "y_mm": 0, "priority": 1}, - {"x_mm": CSU_WIDTH + 1, "y_mm": 0, "priority": 2}, # Should be removed - ] - mask = SlitMask(stars) - assert len(mask.stars) == 1 - assert mask.stars[0]["x_mm"] == 0 and mask.stars[0]["y_mm"] == 0 - -def test_priority_optimization_per_bar_id(): - stars = [ - {"x_mm":0,"y_mm":1, "priority": 1}, - {"x_mm":0,"y_mm":1, "priority": 3}, # same bar_id, higher priority - {"x_mm":0,"y_mm":-1, "priority": 2}, # different bar_id, does not pass if y_mm is positive 40 - ] - mask = SlitMask(stars) - result = mask.return_mask() - - # Check: 1 star per bar_id - bar_ids = [s["bar_id"] for s in result] - print(bar_ids) - assert len(bar_ids) == len(result) - - # Check: highest priority per group is kept - for star in result: - if star["bar_id"] == mask.stars[0]["bar_id"]: - assert star["priority"] == 3 diff --git a/slitmaskgui/tests/test_offline_mode.py b/slitmaskgui/tests/test_offline_mode.py new file mode 100644 index 0000000..0eb8906 --- /dev/null +++ b/slitmaskgui/tests/test_offline_mode.py @@ -0,0 +1,51 @@ +import pytest +from unittest.mock import patch, Mock +from slitmaskgui.offline_mode import InternetConnectionChecker, OfflineMode, CSUConnectionChecker + + +@pytest.fixture +def setup_internet_checker(): + worker = InternetConnectionChecker() + return worker + + +@pytest.fixture +def setup_csu_connection_checker(): + worker = CSUConnectionChecker() + return worker + + +@pytest.fixture +def setup_offline_mode(): + offline_mode = OfflineMode() + return offline_mode + + +def test_offline_mode_starts_internet_connection_checker(setup_offline_mode,qtbot): + offline_mode = setup_offline_mode + + with qtbot.waitSignal(offline_mode.offline_checker.signals.started, timeout = 2000) as worker: + offline_mode.start_checking_internet_connection() + + assert worker.args == [] + + +def test_worker_signals_internet_connection_connection_status(setup_offline_mode,qtbot): + offline_mode = setup_offline_mode + + with qtbot.waitSignal(offline_mode.offline_checker.signals.connection_status, timeout = 2000) as worker: + offline_mode.start_checking_internet_connection() + + # If connected to the internet this should be False otherwise it should be true + assert worker.args == [False] + + +@pytest.mark.skip(reason="functionality is currently deleted") +def test_worker_signals_csu_connection_status(setup_offline_mode,qtbot): + offline_mode = setup_offline_mode + + with qtbot.waitSignal(offline_mode.csu_connection_checker.signals.connection_status, timeout = 2000) as worker: + offline_mode.check_csu_connection() + + # If connected to CSU this will == True else it will be False + assert worker.args == [False] diff --git a/slitmaskgui/tests/test_slit_position_table.py b/slitmaskgui/tests/test_slit_position_table.py new file mode 100644 index 0000000..7e78d18 --- /dev/null +++ b/slitmaskgui/tests/test_slit_position_table.py @@ -0,0 +1,23 @@ +import pytest +from unittest.mock import patch, Mock +from slitmaskgui.slit_position_table import SlitDisplay + + + +@pytest.fixture +def setup_slit_display_class(qtbot): + slit_display = SlitDisplay() + slit_display.changed_data_dict = {14:2, 15:3} + qtbot.addWidget(slit_display) + return slit_display + + +def test_data_saved_signal(setup_slit_display_class, qtbot): + """ This is in test mask config because it has more to do with the mask config than the slit display (data transfer) """ + test_slit_display = setup_slit_display_class + + with qtbot.waitSignal(test_slit_display.data_changed) as bonker: + test_slit_display.data_saved() + # assert bonker.args != [test_slit_display.changed_data_dict] + + assert test_slit_display.changed_data_dict == {} # cleared diff --git a/slitmaskgui/tests/test_star_list.py b/slitmaskgui/tests/test_star_list.py new file mode 100644 index 0000000..24dc345 --- /dev/null +++ b/slitmaskgui/tests/test_star_list.py @@ -0,0 +1,26 @@ +import pytest +from slitmaskgui.backend.star_list import StarList +from slitmaskgui.backend.mask_gen import SlitMask +import json + + + +@pytest.fixture +def sample_target_list(): + with open('slitmaskgui/tests/testfiles/gaia_target_list.json','r') as file: + return file.read() + +@pytest.fixture +def sample_config_data(): + with open('slitmaskgui/tests/testfiles/gaia_mask_config.json','r') as file: + return json.load(file) + +@pytest.fixture +def initialize_star_list(sample_target_list): + return StarList(sample_target_list,slit_width='0.7',use_center_of_priority=True) + +@pytest.mark.skip(reason="mismatch in decimal places") +def test_send_mask(initialize_star_list,sample_config_data): + payload = initialize_star_list + result = payload.send_mask() + assert result == sample_config_data \ No newline at end of file diff --git a/slitmaskgui/tests/testfiles/gaia_mask_config.json b/slitmaskgui/tests/testfiles/gaia_mask_config.json new file mode 100644 index 0000000..a076807 --- /dev/null +++ b/slitmaskgui/tests/testfiles/gaia_mask_config.json @@ -0,0 +1,756 @@ +[ + { + "bar_id": 14, + "center distance": 3.1102612928008213, + "dec": "-10 01 16.0", + "equinox": "2000.0", + "name": "Gaia_039", + "priority": "915", + "ra": "10 20 08.50", + "slit_width": "0.7", + "vmag": "19.29", + "x_mm": 15.143221514175517, + "y_mm": 134.85923999999702 + }, + { + "bar_id": 15, + "center distance": 3.093029487392763, + "dec": "-10 01 24.7", + "equinox": "2000.0", + "name": "Gaia_048", + "priority": "645", + "ra": "10 20 03.26", + "slit_width": "0.7", + "vmag": "19.43", + "x_mm": -41.13371517680923, + "y_mm": 128.5326000000009 + }, + { + "bar_id": 16, + "center distance": 3.093029487392763, + "dec": "-10 01 24.7", + "equinox": "2000.0", + "name": "Gaia_048", + "priority": "645", + "ra": "10 20 03.26", + "slit_width": "0.7", + "vmag": "19.43", + "x_mm": -41.13371517680923, + "y_mm": 128.5326000000009 + }, + { + "bar_id": 17, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 18, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 19, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 20, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 21, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 22, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 23, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 24, + "center distance": 2.687518037998972, + "dec": "-10 01 40.2", + "equinox": "2000.0", + "name": "Gaia_027", + "priority": "1290", + "ra": "10 20 07.05", + "slit_width": "0.7", + "vmag": "17.05", + "x_mm": -0.42959493658638337, + "y_mm": 117.26100000000031 + }, + { + "bar_id": 25, + "center distance": 1.9309707438471553, + "dec": "-10 02 53.8", + "equinox": "2000.0", + "name": "Gaia_025", + "priority": "1934", + "ra": "10 20 01.96", + "slit_width": "0.7", + "vmag": "19.82", + "x_mm": -55.09555061539051, + "y_mm": 63.7390800000031 + }, + { + "bar_id": 26, + "center distance": 1.5034208977807564, + "dec": "-10 02 57.3", + "equinox": "2000.0", + "name": "Gaia_010", + "priority": "610", + "ra": "10 20 09.29", + "slit_width": "0.7", + "vmag": "17.03", + "x_mm": 23.627721511445234, + "y_mm": 61.19387999999922 + }, + { + "bar_id": 27, + "center distance": 2.2252236942276182, + "dec": "-10 03 02.7", + "equinox": "2000.0", + "name": "Gaia_017", + "priority": "711", + "ra": "10 20 14.39", + "slit_width": "0.7", + "vmag": "20.32", + "x_mm": 78.40107592441427, + "y_mm": 57.266999999999065 + }, + { + "bar_id": 28, + "center distance": 2.2252236942276182, + "dec": "-10 03 02.7", + "equinox": "2000.0", + "name": "Gaia_017", + "priority": "711", + "ra": "10 20 14.39", + "slit_width": "0.7", + "vmag": "20.32", + "x_mm": 78.40107592441427, + "y_mm": 57.266999999999065 + }, + { + "bar_id": 29, + "center distance": 1.5729191084100054, + "dec": "-10 03 24.2", + "equinox": "2000.0", + "name": "Gaia_022", + "priority": "1938", + "ra": "10 20 02.01", + "slit_width": "0.7", + "vmag": "20.46", + "x_mm": -54.558556944639214, + "y_mm": 41.63219999999779 + }, + { + "bar_id": 30, + "center distance": 1.9398451613045917, + "dec": "-10 03 34.8", + "equinox": "2000.0", + "name": "Gaia_032", + "priority": "1665", + "ra": "10 19 59.87", + "slit_width": "0.7", + "vmag": "16.64", + "x_mm": -77.5418860512415, + "y_mm": 33.923880000000764 + }, + { + "bar_id": 31, + "center distance": 0.6638937603949324, + "dec": "-10 03 42.2", + "equinox": "2000.0", + "name": "Gaia_006", + "priority": "67", + "ra": "10 20 06.63", + "slit_width": "0.7", + "vmag": "16.35", + "x_mm": -4.940341770560261, + "y_mm": 28.54260000000039 + }, + { + "bar_id": 32, + "center distance": 0.6001756250895524, + "dec": "-10 03 47.0", + "equinox": "2000.0", + "name": "Gaia_007", + "priority": "214", + "ra": "10 20 06.38", + "slit_width": "0.7", + "vmag": "19.41", + "x_mm": -7.6253101241702135, + "y_mm": 25.052040000000776 + }, + { + "bar_id": 33, + "center distance": 0.6001756250895524, + "dec": "-10 03 47.0", + "equinox": "2000.0", + "name": "Gaia_007", + "priority": "214", + "ra": "10 20 06.38", + "slit_width": "0.7", + "vmag": "19.41", + "x_mm": -7.6253101241702135, + "y_mm": 25.052040000000776 + }, + { + "bar_id": 34, + "center distance": 2.4298569501975606, + "dec": "-10 04 05.2", + "equinox": "2000.0", + "name": "Gaia_049", + "priority": "978", + "ra": "10 19 57.28", + "slit_width": "0.7", + "vmag": "16.00", + "x_mm": -105.35815819438567, + "y_mm": 11.817000000000103 + }, + { + "bar_id": 35, + "center distance": 1.9034769893936536, + "dec": "-10 04 10.9", + "equinox": "2000.0", + "name": "Gaia_036", + "priority": "508", + "ra": "10 19 59.39", + "slit_width": "0.7", + "vmag": "15.13", + "x_mm": -82.69702529020485, + "y_mm": 7.671960000002013 + }, + { + "bar_id": 36, + "center distance": 1.9034769893936536, + "dec": "-10 04 10.9", + "equinox": "2000.0", + "name": "Gaia_036", + "priority": "508", + "ra": "10 19 59.39", + "slit_width": "0.7", + "vmag": "15.13", + "x_mm": -82.69702529020485, + "y_mm": 7.671960000002013 + }, + { + "bar_id": 37, + "center distance": 0.3765302659994752, + "dec": "-10 04 27.7", + "equinox": "2000.0", + "name": "Gaia_002", + "priority": "1508", + "ra": "10 20 08.56", + "slit_width": "0.7", + "vmag": "18.93", + "x_mm": 15.787613919018462, + "y_mm": -4.544999999998966 + }, + { + "bar_id": 38, + "center distance": 0.4247702862573389, + "dec": "-10 04 34.8", + "equinox": "2000.0", + "name": "Gaia_003", + "priority": "341", + "ra": "10 20 08.56", + "slit_width": "0.7", + "vmag": "18.42", + "x_mm": 15.787613919018462, + "y_mm": -9.70811999999675 + }, + { + "bar_id": 39, + "center distance": 2.0979063310433976, + "dec": "-10 04 47.4", + "equinox": "2000.0", + "name": "Gaia_041", + "priority": "1000", + "ra": "10 19 58.75", + "slit_width": "0.7", + "vmag": "20.33", + "x_mm": -89.57054427529394, + "y_mm": -18.870839999998648 + }, + { + "bar_id": 40, + "center distance": 0.6056801449533792, + "dec": "-10 04 56.5", + "equinox": "2000.0", + "name": "Gaia_014", + "priority": "852", + "ra": "10 20 06.44", + "slit_width": "0.7", + "vmag": "17.92", + "x_mm": -6.980917719327269, + "y_mm": -25.488359999996657 + }, + { + "bar_id": 41, + "center distance": 0.7597476075156487, + "dec": "-10 04 59.4", + "equinox": "2000.0", + "name": "Gaia_018", + "priority": "204", + "ra": "10 20 05.38", + "slit_width": "0.7", + "vmag": "16.11", + "x_mm": -18.365183538463505, + "y_mm": -27.59724000000001 + }, + { + "bar_id": 42, + "center distance": 0.9546227019504319, + "dec": "-10 05 15.2", + "equinox": "2000.0", + "name": "Gaia_015", + "priority": "190", + "ra": "10 20 08.43", + "slit_width": "0.7", + "vmag": "19.98", + "x_mm": 14.391430375167662, + "y_mm": -39.086999999998554 + }, + { + "bar_id": 43, + "center distance": 0.9546227019504319, + "dec": "-10 05 15.2", + "equinox": "2000.0", + "name": "Gaia_015", + "priority": "190", + "ra": "10 20 08.43", + "slit_width": "0.7", + "vmag": "19.98", + "x_mm": 14.391430375167662, + "y_mm": -39.086999999998554 + }, + { + "bar_id": 44, + "center distance": 1.7462401172375492, + "dec": "-10 05 31.1", + "equinox": "2000.0", + "name": "Gaia_031", + "priority": "1099", + "ra": "10 20 01.79", + "slit_width": "0.7", + "vmag": "19.00", + "x_mm": -56.921329095827694, + "y_mm": -50.649480000001056 + }, + { + "bar_id": 45, + "center distance": 1.9896384038439638, + "dec": "-10 05 33.6", + "equinox": "2000.0", + "name": "Gaia_040", + "priority": "1532", + "ra": "10 20 00.65", + "slit_width": "0.7", + "vmag": "20.53", + "x_mm": -69.1647847881367, + "y_mm": -52.4674800000025 + }, + { + "bar_id": 46, + "center distance": 1.9896384038439638, + "dec": "-10 05 33.6", + "equinox": "2000.0", + "name": "Gaia_040", + "priority": "1532", + "ra": "10 20 00.65", + "slit_width": "0.7", + "vmag": "20.53", + "x_mm": -69.1647847881367, + "y_mm": -52.4674800000025 + }, + { + "bar_id": 47, + "center distance": 1.9870196089137158, + "dec": "-10 05 54.7", + "equinox": "2000.0", + "name": "Gaia_037", + "priority": "427", + "ra": "10 20 02.06", + "slit_width": "0.7", + "vmag": "18.67", + "x_mm": -54.02156327396118, + "y_mm": -67.81140000000188 + }, + { + "bar_id": 48, + "center distance": 1.7950628502934136, + "dec": "-10 06 00.4", + "equinox": "2000.0", + "name": "Gaia_028", + "priority": "1119", + "ra": "10 20 04.21", + "slit_width": "0.7", + "vmag": "20.11", + "x_mm": -30.930835433193977, + "y_mm": -71.95643999999997 + }, + { + "bar_id": 49, + "center distance": 1.9571186264810672, + "dec": "-10 06 11.9", + "equinox": "2000.0", + "name": "Gaia_024", + "priority": "532", + "ra": "10 20 09.79", + "slit_width": "0.7", + "vmag": "18.89", + "x_mm": 28.997658218591884, + "y_mm": -80.31924000000011 + }, + { + "bar_id": 50, + "center distance": 2.48721751951569, + "dec": "-10 06 14.2", + "equinox": "2000.0", + "name": "Gaia_026", + "priority": "445", + "ra": "10 20 13.71", + "slit_width": "0.7", + "vmag": "19.61", + "x_mm": 71.09796200266553, + "y_mm": -81.99179999999828 + }, + { + "bar_id": 51, + "center distance": 2.48721751951569, + "dec": "-10 06 14.2", + "equinox": "2000.0", + "name": "Gaia_026", + "priority": "445", + "ra": "10 20 13.71", + "slit_width": "0.7", + "vmag": "19.61", + "x_mm": 71.09796200266553, + "y_mm": -81.99179999999828 + }, + { + "bar_id": 52, + "center distance": 2.5413817486248407, + "dec": "-10 06 32.4", + "equinox": "2000.0", + "name": "Gaia_035", + "priority": "536", + "ra": "10 20 12.38", + "slit_width": "0.7", + "vmag": "18.21", + "x_mm": 56.813930361662784, + "y_mm": -95.22683999999896 + }, + { + "bar_id": 53, + "center distance": 3.1189343725305068, + "dec": "-10 06 41.3", + "equinox": "2000.0", + "name": "Gaia_047", + "priority": "326", + "ra": "10 20 15.51", + "slit_width": "0.7", + "vmag": "20.70", + "x_mm": 90.42973414839345, + "y_mm": -101.69891999999834 + }, + { + "bar_id": 54, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 55, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 56, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 57, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 58, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 59, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 60, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 61, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 62, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 63, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 64, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 65, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 66, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 67, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 68, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 69, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 70, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + }, + { + "bar_id": 71, + "center distance": 2.547159624063119, + "dec": "-10 06 47.4", + "equinox": "2000.0", + "name": "Gaia_050", + "priority": "1453", + "ra": "10 20 04.02", + "slit_width": "0.7", + "vmag": "19.73", + "x_mm": -32.97141138196098, + "y_mm": -106.13483999999833 + } +] \ No newline at end of file diff --git a/slitmaskgui/tests/testfiles/gaia_target_list.json b/slitmaskgui/tests/testfiles/gaia_target_list.json new file mode 100644 index 0000000..5e490e5 --- /dev/null +++ b/slitmaskgui/tests/testfiles/gaia_target_list.json @@ -0,0 +1,402 @@ +[ + { + "name": "Gaia_001", + "ra": "10 20 08.16", + "dec": "-10 04 04.6", + "equinox": "2000.0", + "vmag": "19.58", + "priority": "403" + }, + { + "name": "Gaia_002", + "ra": "10 20 08.56", + "dec": "-10 04 27.7", + "equinox": "2000.0", + "vmag": "18.93", + "priority": "1508" + }, + { + "name": "Gaia_003", + "ra": "10 20 08.56", + "dec": "-10 04 34.8", + "equinox": "2000.0", + "vmag": "18.42", + "priority": "341" + }, + { + "name": "Gaia_004", + "ra": "10 20 07.44", + "dec": "-10 03 23.7", + "equinox": "2000.0", + "vmag": "18.87", + "priority": "1102" + }, + { + "name": "Gaia_005", + "ra": "10 20 09.38", + "dec": "-10 03 08.1", + "equinox": "2000.0", + "vmag": "19.40", + "priority": "1255" + }, + { + "name": "Gaia_006", + "ra": "10 20 06.63", + "dec": "-10 03 42.2", + "equinox": "2000.0", + "vmag": "16.35", + "priority": "67" + }, + { + "name": "Gaia_007", + "ra": "10 20 06.38", + "dec": "-10 03 47.0", + "equinox": "2000.0", + "vmag": "19.41", + "priority": "214" + }, + { + "name": "Gaia_008", + "ra": "10 20 13.93", + "dec": "-10 04 14.1", + "equinox": "2000.0", + "vmag": "19.04", + "priority": "1087" + }, + { + "name": "Gaia_009", + "ra": "10 20 11.81", + "dec": "-10 04 53.9", + "equinox": "2000.0", + "vmag": "20.07", + "priority": "1399" + }, + { + "name": "Gaia_010", + "ra": "10 20 09.29", + "dec": "-10 02 57.3", + "equinox": "2000.0", + "vmag": "17.03", + "priority": "610" + }, + { + "name": "Gaia_011", + "ra": "10 20 12.89", + "dec": "-10 04 55.2", + "equinox": "2000.0", + "vmag": "18.33", + "priority": "240" + }, + { + "name": "Gaia_012", + "ra": "10 20 07.47", + "dec": "-10 02 59.7", + "equinox": "2000.0", + "vmag": "19.44", + "priority": "1095" + }, + { + "name": "Gaia_013", + "ra": "10 20 07.80", + "dec": "-10 02 53.9", + "equinox": "2000.0", + "vmag": "19.01", + "priority": "1662" + }, + { + "name": "Gaia_014", + "ra": "10 20 06.44", + "dec": "-10 04 56.5", + "equinox": "2000.0", + "vmag": "17.92", + "priority": "852" + }, + { + "name": "Gaia_015", + "ra": "10 20 08.43", + "dec": "-10 05 15.2", + "equinox": "2000.0", + "vmag": "19.98", + "priority": "190" + }, + { + "name": "Gaia_016", + "ra": "10 20 05.91", + "dec": "-10 04 55.5", + "equinox": "2000.0", + "vmag": "16.19", + "priority": "674" + }, + { + "name": "Gaia_017", + "ra": "10 20 14.39", + "dec": "-10 03 02.7", + "equinox": "2000.0", + "vmag": "20.32", + "priority": "711" + }, + { + "name": "Gaia_018", + "ra": "10 20 05.38", + "dec": "-10 04 59.4", + "equinox": "2000.0", + "vmag": "16.11", + "priority": "204" + }, + { + "name": "Gaia_019", + "ra": "10 20 15.14", + "dec": "-10 03 03.5", + "equinox": "2000.0", + "vmag": "19.49", + "priority": "1493" + }, + { + "name": "Gaia_020", + "ra": "10 20 17.35", + "dec": "-10 03 45.6", + "equinox": "2000.0", + "vmag": "19.46", + "priority": "1460" + }, + { + "name": "Gaia_021", + "ra": "10 20 02.42", + "dec": "-10 04 49.5", + "equinox": "2000.0", + "vmag": "20.14", + "priority": "449" + }, + { + "name": "Gaia_022", + "ra": "10 20 02.01", + "dec": "-10 03 24.2", + "equinox": "2000.0", + "vmag": "20.46", + "priority": "1938" + }, + { + "name": "Gaia_023", + "ra": "10 20 02.56", + "dec": "-10 05 06.1", + "equinox": "2000.0", + "vmag": "17.47", + "priority": "1719" + }, + { + "name": "Gaia_024", + "ra": "10 20 09.79", + "dec": "-10 06 11.9", + "equinox": "2000.0", + "vmag": "18.89", + "priority": "532" + }, + { + "name": "Gaia_025", + "ra": "10 20 01.96", + "dec": "-10 02 53.8", + "equinox": "2000.0", + "vmag": "19.82", + "priority": "1934" + }, + { + "name": "Gaia_026", + "ra": "10 20 13.71", + "dec": "-10 06 14.2", + "equinox": "2000.0", + "vmag": "19.61", + "priority": "445" + }, + { + "name": "Gaia_027", + "ra": "10 20 07.05", + "dec": "-10 01 40.2", + "equinox": "2000.0", + "vmag": "17.05", + "priority": "1290" + }, + { + "name": "Gaia_028", + "ra": "10 20 04.21", + "dec": "-10 06 00.4", + "equinox": "2000.0", + "vmag": "20.11", + "priority": "1119" + }, + { + "name": "Gaia_029", + "ra": "10 20 19.86", + "dec": "-10 04 24.6", + "equinox": "2000.0", + "vmag": "20.60", + "priority": "1" + }, + { + "name": "Gaia_030", + "ra": "10 20 00.11", + "dec": "-10 03 35.0", + "equinox": "2000.0", + "vmag": "15.27", + "priority": "772" + }, + { + "name": "Gaia_031", + "ra": "10 20 01.79", + "dec": "-10 05 31.1", + "equinox": "2000.0", + "vmag": "19.00", + "priority": "1099" + }, + { + "name": "Gaia_032", + "ra": "10 19 59.87", + "dec": "-10 03 34.8", + "equinox": "2000.0", + "vmag": "16.64", + "priority": "1665" + }, + { + "name": "Gaia_033", + "ra": "10 20 20.56", + "dec": "-10 04 01.4", + "equinox": "2000.0", + "vmag": "18.31", + "priority": "368" + }, + { + "name": "Gaia_034", + "ra": "10 20 04.05", + "dec": "-10 06 09.1", + "equinox": "2000.0", + "vmag": "20.44", + "priority": "1935" + }, + { + "name": "Gaia_035", + "ra": "10 20 12.38", + "dec": "-10 06 32.4", + "equinox": "2000.0", + "vmag": "18.21", + "priority": "536" + }, + { + "name": "Gaia_036", + "ra": "10 19 59.39", + "dec": "-10 04 10.9", + "equinox": "2000.0", + "vmag": "15.13", + "priority": "508" + }, + { + "name": "Gaia_037", + "ra": "10 20 02.06", + "dec": "-10 05 54.7", + "equinox": "2000.0", + "vmag": "18.67", + "priority": "427" + }, + { + "name": "Gaia_038", + "ra": "10 20 05.59", + "dec": "-10 06 32.3", + "equinox": "2000.0", + "vmag": "17.44", + "priority": "1578" + }, + { + "name": "Gaia_039", + "ra": "10 20 08.50", + "dec": "-10 01 16.0", + "equinox": "2000.0", + "vmag": "19.29", + "priority": "915" + }, + { + "name": "Gaia_040", + "ra": "10 20 00.65", + "dec": "-10 05 33.6", + "equinox": "2000.0", + "vmag": "20.53", + "priority": "1532" + }, + { + "name": "Gaia_041", + "ra": "10 19 58.75", + "dec": "-10 04 47.4", + "equinox": "2000.0", + "vmag": "20.33", + "priority": "1000" + }, + { + "name": "Gaia_042", + "ra": "10 20 21.54", + "dec": "-10 04 31.1", + "equinox": "2000.0", + "vmag": "18.33", + "priority": "764" + }, + { + "name": "Gaia_043", + "ra": "10 20 21.66", + "dec": "-10 04 29.9", + "equinox": "2000.0", + "vmag": "16.38", + "priority": "1129" + }, + { + "name": "Gaia_044", + "ra": "10 20 10.73", + "dec": "-10 06 54.5", + "equinox": "2000.0", + "vmag": "20.22", + "priority": "1062" + }, + { + "name": "Gaia_045", + "ra": "10 20 19.43", + "dec": "-10 02 13.7", + "equinox": "2000.0", + "vmag": "20.46", + "priority": "780" + }, + { + "name": "Gaia_046", + "ra": "10 20 00.07", + "dec": "-10 05 42.7", + "equinox": "2000.0", + "vmag": "17.50", + "priority": "1024" + }, + { + "name": "Gaia_047", + "ra": "10 20 15.51", + "dec": "-10 06 41.3", + "equinox": "2000.0", + "vmag": "20.70", + "priority": "326" + }, + { + "name": "Gaia_048", + "ra": "10 20 03.26", + "dec": "-10 01 24.7", + "equinox": "2000.0", + "vmag": "19.43", + "priority": "645" + }, + { + "name": "Gaia_049", + "ra": "10 19 57.28", + "dec": "-10 04 05.2", + "equinox": "2000.0", + "vmag": "16.00", + "priority": "978" + }, + { + "name": "Gaia_050", + "ra": "10 20 04.02", + "dec": "-10 06 47.4", + "equinox": "2000.0", + "vmag": "19.73", + "priority": "1453" + } +] \ No newline at end of file diff --git a/slitmaskgui/tests/testfiles/star_list.txt b/slitmaskgui/tests/testfiles/star_list.txt deleted file mode 100644 index dfb9074..0000000 --- a/slitmaskgui/tests/testfiles/star_list.txt +++ /dev/null @@ -1,4 +0,0 @@ -Gaia_001 15 25 32.35 -50 46 46.8 2000.0 vmag=20.77 priority=1020 -Gaia_001 15 25 32.35 -50 46 46.8 2000.0 priority=1020 vmag=20.77 -Gaia_011 15 25 32.35 -50 46 46.8 2000.0 vmag=20.77 priority=1020 -Gaia_021 15 25 32.35 -50 46 46.8 2000.0 vmag=20.77 priority=1020 must_have=True \ No newline at end of file