diff --git a/.gitignore b/.gitignore index fabc023..3a890f1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,10 @@ build jandata classconverter classdata +discos2class.egg-info/ src/__init__.py.bak src/discosscan.py.bak +gui/scan_cycle.txt *.bak dist/ build/ diff --git a/README.md b/README.md index fbb6c01..cfec853 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,89 @@ -# discos2class +# discos2class - GUI Mode + +This section describes how to run the graphical user interface (GUI) of the software. + +--- + +## Requirements + +- Python **>= 3.10,< 3.13** (tested with **3.11.8**) +- Dependencies listed in `requirements.txt` + +--- + +## Installation + +Create a virtual environment and install the required dependencies. + +### Linux / macOS + +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### Windows (Git Bash) + +```bash +python -m venv venv +source venv/Scripts/activate +pip install -r requirements.txt +``` + +## Running the GUI + +From the **root directory of the project**, run: + +```bash +python -m gui.d2c_gui_loader_v2_0 + +Important: do not run the file directly. +The GUI must be executed as a Python module using the -m option. +``` + +### Configuration file (`config.ini`) + +The application relies on the `config.ini` file to define the available data backends. +This file **must be configured properly** in order for the application to work as expected. + +Each backend is defined using three parallel lists: + +- `labels`: names shown in the GUI ComboBox +- `paths`: filesystem paths where the data is stored +- `ips`: IP addresses of the servers hosting the data + +All lists **must have the same number of elements** and be **index-aligned**. +The same index across the three lists refers to the same backend. + +Example: + +```ini +[Drives] +labels = Backend A, Backend B +paths = /mnt/data_a, /mnt/data_b +ips = 192.168.1.10, 192.168.1.20 +``` + +#### Runtime checks + +At runtime, the application performs the following checks for the selected backend: + +1. Verifies that the selected server (IP) is reachable. +2. Verifies that the corresponding filesystem path exists and is correctly mounted. +3. Proceeds with data analysis **only if** the drive is accessible. + +An incorrect or inconsistent configuration (for example mismatched list lengths, +invalid paths, or unreachable servers) may prevent the application from accessing +the data. + + + + +# discos2class - Command-Line Mode **discos2class** command line tool used to -convert DISCOS spectroscopy files acquired with XARCOS into CLASS native format. +convert DISCOS spectroscopy files acquired with XARCOS, SARDARA and SKARAB into CLASS native format. ##TOC @@ -203,7 +285,7 @@ compute the resulting spectrum as: ##Requirements -The software is developed in Python, using python2.7, and it depends on +The software is developed in Python, using python3.11, and it depends on external python packages: * astropy @@ -222,7 +304,7 @@ The command line tool can be installed via: $ python setup.py install ``` -Tested with Python 3.11.8 +Tested with Python 3.11 diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/config.ini b/gui/config.ini index 3996dd5..882d6d0 100644 --- a/gui/config.ini +++ b/gui/config.ini @@ -1,3 +1,5 @@ -[Paths] -path = /roach2_nuraghe/data/,/discos-archive/data/ +[DRIVES] +labels = SARDARA_BKD,SKARAB_BKD +paths = /roach2_nuraghe/data/,/discos-archive/data/ +ips = 192.168.1.10,192.168.1.20 diff --git a/gui/d2c_gui_loader_v2_0.py b/gui/d2c_gui_loader_v2_0.py index a3cc94f..94a84e0 100644 --- a/gui/d2c_gui_loader_v2_0.py +++ b/gui/d2c_gui_loader_v2_0.py @@ -1,28 +1,34 @@ +# VERSION DATE: 09-02-2026 + import configparser import argparse +import math import os -import time import subprocess import sys +import time from astropy.io import fits from astropy.time import Time -from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtWidgets import QMainWindow, QApplication, QProgressBar -from PyQt5.QtCore import pyqtSignal, QDateTime, QThread, QTimer, QTime -from PyQt5.uic import loadUi from tkinter import filedialog -# VERSION DATE: 18-03-2025 +from services.duty_cycle import DutyCycle +from services.file_services import FileServices +from services.drive_service import check_drive_status -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.dirname(SCRIPT_DIR)) -# Add the subfolder to sys.path -sys.path.append(os.path.join(SCRIPT_DIR, 'src')) -# The previous line allows to call modules without specifying the subfolders where they are located +# Sometimes the 'XDG_RUNTIME_DIR' is not in the system and Qt may complain although the app runs smoothly +# The following lines of code, in case, creates the variable which Qt is looking for +if "XDG_RUNTIME_DIR" not in os.environ: + runtime_dir = f"/tmp/runtime-{os.getuid()}" + os.makedirs(runtime_dir, exist_ok=True) + os.chmod(runtime_dir, 0o700) + os.environ["XDG_RUNTIME_DIR"] = runtime_dir + +from PyQt5.QtGui import QStandardItemModel, QStandardItem, QColor +from PyQt5.QtWidgets import QMainWindow, QApplication, QProgressBar +from PyQt5.QtCore import pyqtSignal, QDateTime, QThread, QTimer, QTime +from PyQt5.uic import loadUi -from duty_cycle import DutyCycle -from file_services import FileServices class CheckWorkerThread(QThread): @@ -33,7 +39,7 @@ class CheckWorkerThread(QThread): # Signal to notify the main thread to disable/enable the button in the interface update_button_signal = pyqtSignal(bool) # Signal to notify the main thread (UI) to update the console (QListView) in the interface - update_console_lv_signal = pyqtSignal(str) + update_console_lv_signal = pyqtSignal(str, QColor) # Signal to notify the main thread (UI) to update the progressbar range values update_check_pb_range = pyqtSignal(QProgressBar, int, int) # Signal to notify the main thread (UI) to update the progressbar current value @@ -65,7 +71,7 @@ def get_len_subscans(self): def run(self): # Disable the button when the thread starts - self.update_console_lv_signal.emit("Duty cycle check started. Please wait...") + self.update_console_lv_signal.emit("Duty cycle check started. Please wait...", QColor("black")) folders_to_scan = [] @@ -89,7 +95,7 @@ def run(self): self.subscans.clear() - self.update_console_lv_signal.emit("Checking SOURCE FOLDER: " + folders_to_scan[f]) + self.update_console_lv_signal.emit("Checking SOURCE FOLDER: " + folders_to_scan[f], QColor("black")) # Get useful information relative to each scan contained in the selected source folder for subscan_file in os.listdir(input_scan_directory): @@ -161,7 +167,8 @@ def run(self): filename_err = self.subscans[i][0] check_result[0] = error - check_result[1] = d + #check_result[1] = d + check_result[1] = math.floor(i/len(self.subscans)) check_result[2] = filename_err f = len(folders_to_scan) @@ -252,7 +259,7 @@ class MainUI(QMainWindow): # Signal to notify the main thread to disable/enable the button in the interface update_button_signal = pyqtSignal(bool) # Signal to notify the main thread (UI) to update the console (QListView) in the interface - update_console_lv_signal = pyqtSignal(str) + update_console_lv_signal = pyqtSignal(str, QColor) # Signal to notify the main thread (UI) to update the progressbar range values update_progressbar_range = pyqtSignal(int, int) # Signal to notify the main thread (UI) to update the progressbar current value @@ -267,15 +274,18 @@ class MainUI(QMainWindow): progress_timer = False scan_cycles = [] # an array containing the number of scan cycles per each folder - # List definitions + # List definitions + # If you want to add a TEST item which points to data in the home02 folder then uncomment the following line + # Next, add the home02 full-path of data as third item in the list of paths inside the config.ini file # SERVER_BACKEND = ['SARDARA_BKD', 'SKARAB_BKD', 'TEST'] - SERVER_BACKEND = ['SARDARA_BKD', 'SKARAB_BKD'] - SERVER_PING = ['192.168.200.216', '192.168.203.36', '192.168.200.216'] + #SERVER_BACKEND = ['SARDARA_BKD', 'SKARAB_BKD'] + #SERVER_PING = ['192.168.200.216', '192.168.203.36', '192.168.200.216'] + paths = None # paths of the mounted drives where data are located + ips = None # ips relative to servers DUTY_CYCLE_VALUES = ['0','1','2','3','4','5','6','7','8','9'] MODE_TYPE = ['POSITION SWITCHING', 'NODDING'] COMBO_MSGs = ['NOT AVAILABLE'] - # path_to_spectral_line_data = ["/roach2_nuraghe/data/", "discos-archive/data"] # index connected to the backend chosen def __init__(self): @@ -300,8 +310,6 @@ def __init__(self): self.debug_on = False - - # Get the current working directory (the directory where the script is run from) # current_directory = os.getcwd() # print("Current working directory:", current_directory) @@ -311,12 +319,8 @@ def __init__(self): print("Script is located at:", self.script_directory) # Check if the config.ini file exists otherwise initialize it. Retrieve the paths of the mounted drives - self.path_to_spectral_line_data = self.check_config_exists(self.script_directory, 'config.ini') + self.labels, self.paths, self.ips = self.check_config_exists(self.script_directory, 'config.ini') - - - - # Set the fixed size of the window (width, height) self.setFixedSize(1212, 747) # Set the size to 800x600 pixels @@ -350,7 +354,7 @@ def __init__(self): self.update_progress_bar_value(self.start_pb, 0) # Adding combobox drop down list values - self.backend_cmb.addItems(self.SERVER_BACKEND) + self.backend_cmb.addItems(self.labels) self.mode_cmb.addItems(self.MODE_TYPE) self.refsig_cmb.addItems(self.DUTY_CYCLE_VALUES) self.signal_cmb.addItems(self.DUTY_CYCLE_VALUES) @@ -364,11 +368,8 @@ def __init__(self): self.refsig_cmb.setMaxVisibleItems(5) self.signal_cmb.setMaxVisibleItems(5) self.ref_cmb.setMaxVisibleItems(5) - self.refcal_cmb.setMaxVisibleItems(5) - - # Load all projects id related to the path_to_spectral_line_data and all relative folders (level 0 and 1) - self.update_projects_id(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()]) - + self.refcal_cmb.setMaxVisibleItems(5) + # Adding actions to comboboxes self.project_id_cmb.activated.connect(self.update_folders_level0) self.folder0_cmb.activated.connect(self.update_folders_level1) @@ -379,13 +380,20 @@ def __init__(self): self.refcal_cmb.activated.connect(self.enable_check_btn) #self.refsig_cmb_sl.activated.connect(lambda: self.enableCheckBtn(True) if self.getDutyCycleSize() > 0 else self.enableVeryfyBtn()) self.folder1_cmb.currentIndexChanged.connect(lambda: self.start_btn.setEnabled(False)) - self.backend_cmb.currentIndexChanged.connect(lambda: self.check_server(self.SERVER_PING[self.backend_cmb.currentIndex()])) + #self.backend_cmb.currentIndexChanged.connect(lambda: self.check_server(self.SERVER_PING[self.backend_cmb.currentIndex()])) + self.backend_cmb.currentIndexChanged.connect(self.on_drive_changed) # Qt passes the index automatically + + self.backend_cmb.currentIndexChanged.connect(self.enable_check_btn) # Qt passes the index automatically + self.folder0_cmb.currentIndexChanged.connect(self.enable_check_btn) # Qt passes the index automatically + self.folder1_cmb.currentIndexChanged.connect(self.enable_check_btn) # Qt passes the index automatically + + # Buttons actions self.check_btn.clicked.connect(self.check_data) self.start_btn.clicked.connect(self.convert_data) self.dest_btn.clicked.connect(self.get_dest_folder) - self.update_btn.clicked.connect(lambda: self.update_projects_id(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()])) + self.update_btn.clicked.connect(lambda: self.update_projects_id(self.paths[self.backend_cmb.currentIndex()])) # Check Boxes actions self.all_folders_cb.clicked.connect(lambda: self.start_btn.setEnabled(False)) @@ -412,46 +420,55 @@ def __init__(self): # Add the item to the model (which updates the QListView) self.model.appendRow(init_date_time_item) + # Load all projects id related to the path_to_spectral_line_data and all relative folders (level 0 and 1) + # self.update_projects_id(self.paths[self.backend_cmb.currentIndex()]) + self.on_drive_changed(0) + + def check_config_exists(self, app_path, filename_ini): - paths = None - - full_path_ini = app_path + '/' + filename_ini - # if config.ini exists then load the mounted drives, otherwise create the config.ini with the list of mounted drives [it should be one drive per backend] - + full_path_ini = os.path.join(app_path, filename_ini) + + def parse_list(value): + return [x.strip() for x in value.split(",")] + if os.path.exists(full_path_ini): - - print(f"The file '{full_path_ini}' exists. Retrieving mounted drives...") - # Read out the mounted drives from the config.ini file - # Create a ConfigParser object + + print(f"The file '{full_path_ini}' exists. Retrieving drives...") + config = configparser.ConfigParser() - # Read the configuration file config.read(full_path_ini) - # Get the comma-separated string of paths under the 'Paths' section - paths_str = config.get('Paths', 'PATH') - # Split the string into a list of paths - paths = paths_str.split(',') - + + labels = parse_list(config.get('DRIVES', 'labels')) + paths = parse_list(config.get('DRIVES', 'paths')) + ips = parse_list(config.get('DRIVES', 'ips')) + + # sicurezza: liste allineate + if len({len(labels), len(paths), len(ips)}) != 1: + raise ValueError("config.ini non valido: labels, paths e ips non allineati") + else: - - paths = ["/roach2_nuraghe/data/", "/discos-archive/data/"] print(f"The file '{full_path_ini}' does not exist. Initialization started...") - # config.ini initialization - # Create a ConfigParser object + + labels = ['SARDARA_BKD', 'SKARAB_BKD'] + paths = ["/roach2_nuraghe/data/", "/discos-archive/data/"] + ips = ["192.168.1.10", "192.168.1.20"] + config = configparser.ConfigParser() - # Add a section for PATH - config.add_section('Paths') - # Write the list of paths under the 'Paths' section, as a comma-separated string - config.set('Paths', 'PATH', ','.join(paths)) - # Write the configuration to a file - with open(full_path_ini, 'w') as configfile: - config.write(configfile) + config.add_section('DRIVES') + config.set('DRIVES', 'labels', ','.join(labels)) + config.set('DRIVES', 'paths', ','.join(paths)) + config.set('DRIVES', 'ips', ','.join(ips)) - return paths + with open(full_path_ini, 'w') as configfile: + config.write(configfile) + # return data + return labels, paths, ips + def check_data(self): @@ -501,9 +518,9 @@ def check_data(self): duty_cycle_flags.append('REFCAL') # Create and start the worker thread, passing the parameter (skip_calibration) to it - self.thread = CheckWorkerThread(self.check_pb, self.path_to_spectral_line_data[self.backend_cmb.currentIndex()], self.project_id_cmb.currentText(), self.folder0_cmb.currentText(), + self.thread = CheckWorkerThread(self.check_pb, self.paths[self.backend_cmb.currentIndex()], self.project_id_cmb.currentText(), self.folder0_cmb.currentText(), self.folder1_cmb.currentText(), self.folder1_cmb, self.all_folders_cb.isChecked(), duty_cycle, duty_cycle_flags, duty_cycle_size, mode, True) - #self.thread = CheckWorkerThread(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()], self.project_id_cmb.currentText(), self.folder0_cmb.currentText(), + #self.thread = CheckWorkerThread(self.paths[self.backend_cmb.currentIndex()], self.project_id_cmb.currentText(), self.folder0_cmb.currentText(), # folders_to_scan[i], duty_cycle, duty_cycle_flags, duty_cycle_size, mode, True) # Connect the signal from the worker thread to enable/disable the button @@ -526,28 +543,31 @@ def check_data_result(self, error, check_result, subscans, duty_cycle_size): if(error): - self.update_console_lv('FILE ERROR - [DUTY CYCLE: ' + str(check_result[1]) + ', FILE: ' + check_result[2] + '. Please try again.') - self.update_console_lv("") + self.update_console_lv('DUTY CYCLE ERROR - [NUMBER: ' + str(check_result[1]) + ', FILE: ' + check_result[2] + '.', QColor("red")) + self.update_console_lv('WARNING: Data conversion may provide wrong results!', QColor("orange")) + #self.update_console_lv("", QColor("black")) self.enable_check_btn() - - else: + # Get the parameters anyway to start the process - # After disabling the combo mode restore it to the prevuious value - if(self.mode_cmb.currentText() == self.MODE_TYPE[1]): - self.enable_nodding(True) - else: - self.enable_nodding(False) + #else: - n_duty_cycles = int(subscans / duty_cycle_size) - self.update_console_lv("DUTY CYCLES FOUND [" + str(n_duty_cycles) + "]. Data check successfully completed") - self.update_console_lv("") + # After disabling the combo mode restore it to the prevuious value + if(self.mode_cmb.currentText() == self.MODE_TYPE[1]): + self.enable_nodding(True) + else: + self.enable_nodding(False) - # Enable the process button - self.start_btn.setEnabled(True) + n_duty_cycles = int(subscans / duty_cycle_size) + self.update_console_lv("DUTY CYCLES FOUND [" + str(n_duty_cycles) + "]. Data check completed", QColor("black")) + self.update_console_lv("", QColor("black")) - # Set the number of subscan per folder - self.n_subscans = subscans - self.scan_cycles.append(n_duty_cycles) + # Enable the process button + self.start_btn.setEnabled(True) + + # Set the number of subscan per folder + self.n_subscans = subscans + #print('SCAN CYCLE PAR', subscans, duty_cycle_size, n_duty_cycles) + self.scan_cycles.append(n_duty_cycles) # Scroll to the bottom of the list view self.scroll_to_bottom() @@ -560,7 +580,7 @@ def check_server(self, remote_ip): if(server_up): - self.update_projects_id(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()]) + self.update_projects_id(self.paths[self.backend_cmb.currentIndex()]) else: @@ -645,12 +665,12 @@ def get_dest_folder(self): if(self.destination_folder): - self.update_console_lv('Selected DESTINATION FOLDER: ' + self.destination_folder) + self.update_console_lv('Selected DESTINATION FOLDER: ' + self.destination_folder, QColor("black")) self.enable_check_btn() else: - self.update_console_lv('Selected DESTINATION FOLDER: ') + self.update_console_lv('Selected DESTINATION FOLDER: ', QColor("black")) self.destination_folder = "" @@ -692,12 +712,12 @@ def on_task_done(self, result): if(result): # case of errors - self.update_console_lv("Data processing ended with errors! Please check your data and try again.") + self.update_console_lv("Data processing ended with errors! Please check your data and try again.", QColor("red")) else: - self.update_console_lv('Data processing successfully completed! Enjoy GILDAS :-)') - self.update_console_lv('') + self.update_console_lv('Data processing successfully completed! Enjoy GILDAS :-)', QColor("black")) + self.update_console_lv('', QColor("black")) # Scroll to the bottom of the list view self.scroll_to_bottom() @@ -726,7 +746,7 @@ def convert_data(self): self.current_index = 0 # Disable the button when the thread starts self.update_widgets_state(False) - self.update_console_lv("Data processing started! Please wait...") + self.update_console_lv("Data processing started! Please wait...", QColor("black")) # Create the duty cycle string duty_cycle = self.get_duty_cycle() @@ -743,11 +763,11 @@ def start_next_task(self, folders_to_scan, duty_cycle): if self.current_index < len(folders_to_scan): # Check if we still have tasks to run # Folder to scan - input_scan_directory = (self.path_to_spectral_line_data[self.backend_cmb.currentIndex()] + '/' + self.project_id_cmb.currentText() + '/' + input_scan_directory = (self.paths[self.backend_cmb.currentIndex()] + '/' + self.project_id_cmb.currentText() + '/' + self.folder0_cmb.currentText() + '/' + folders_to_scan[self.current_index]) executable_command = self.d2c_cmd_builder(duty_cycle, self.skip_cal_cb.isChecked(), input_scan_directory, self.destination_folder) - self.update_console_lv_signal.emit("COMMAND BEING EXECUTED -> " + executable_command) + self.update_console_lv_signal.emit("COMMAND BEING EXECUTED -> " + executable_command, QColor("black")) # Scroll to the bottom of the list view self.scroll_to_bottom() @@ -775,7 +795,7 @@ def start_next_task(self, folders_to_scan, duty_cycle): def update_folders_level0(self): # populate the combo boxes containing the sub-folder [level-0] - self.folders_level0 = self.file_services.get_folders(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()] + self.project_id_cmb.currentText(), "0-sl") + self.folders_level0 = self.file_services.get_folders(self.paths[self.backend_cmb.currentIndex()] + self.project_id_cmb.currentText(), "0-sl") # if self.folders_level0 is not empty then populate the relative combo box and try to get the folders relative to level 1 if(len(self.folders_level0) > 0): @@ -794,11 +814,12 @@ def update_folders_level0(self): self.disable_combobox(self.folder0_cmb) self.disable_combobox(self.folder1_cmb) + def update_folders_level1(self): # populate the combo boxes containing the sub-folder [level-0] - self.folders_level1 = self.file_services.get_folders(self.path_to_spectral_line_data[self.backend_cmb.currentIndex()] + self.project_id_cmb.currentText() + self.folders_level1 = self.file_services.get_folders(self.paths[self.backend_cmb.currentIndex()] + self.project_id_cmb.currentText() + "/" + self.folder0_cmb.currentText(), "1-sl") # if self.folders_level1 is not empty then populate the relative combobox @@ -830,7 +851,7 @@ def update_folders_level1(self): - def update_console_lv(self, string: str): + def update_console_lv(self, string: str, color: QColor): current_date = QDateTime.currentDateTime().toString("dd-MM-yyyy") # Get the current time (you can also adjust the format if you want to include time) @@ -840,7 +861,10 @@ def update_console_lv(self, string: str): # Create a QStandardItem with the current date and time init_msg = f"[{current_date} - {current_time}]: " + string init_msg_item = QStandardItem(init_msg) + # Set the foreground color according to the message ('red' for errors, 'black' otherwise) + init_msg_item.setForeground(color) # Add the item to the model (which updates the QListView) + self.model.appendRow(init_msg_item) scrollbar = self.console_lv.verticalScrollBar() @@ -983,7 +1007,67 @@ def stop_timer(self): except: pass - + + + + def on_drive_changed(self, index): + + if index < 0: + return + + label = self.labels[index] + path = self.paths[index] + ip = self.ips[index] + + status = check_drive_status(path, ip) + + # print(status) + + if status["mounted"]: + + self.update_projects_id(self.paths[self.backend_cmb.currentIndex()]) + + else: + + self.disable_combobox(self.folder0_cmb) + self.disable_combobox(self.folder1_cmb) + self.disable_combobox(self.project_id_cmb) + self.disable_widget([self.update_btn], True) + self.enable_check_btn() + + # Update the console + self.log_drive_status(status, label) + + + + def log_drive_status(self, status, label): + + if status["mounted"]: + msg = ( + f"Data relative to '{label}' available" + #"({status['path']})" + ) + color = QColor("green") + + elif status["reachable"]: + msg = ( + f"Server reachable " + #"({status['ip']}), " + "but Data relative to '{label}' not available" + ) + color = QColor("orange") + + else: + msg = ( + f"Data relative to '{label}' not available " + #f"(path: {status['path']}, ip: {status['ip']})" + ) + color = QColor("red") + + self.update_console_lv(msg, color) + + + if __name__ == "__main__": diff --git a/gui/services/__init__.py b/gui/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/services/drive_service.py b/gui/services/drive_service.py new file mode 100644 index 0000000..4e51a09 --- /dev/null +++ b/gui/services/drive_service.py @@ -0,0 +1,29 @@ +import os +import subprocess + +def check_drive_status(path, ip): + + mounted = ( + os.path.exists(path) and + os.path.isdir(path) and + os.access(path, os.R_OK) + ) + + is_mount = os.path.ismount(path) + + reachable = False + if ip: + result = subprocess.run( + ["ping", "-c", "1", "-W", "1", ip], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + reachable = result.returncode == 0 + + return { + "mounted": mounted, + "is_mount": is_mount, + "reachable": reachable, + "path": path, + "ip": ip + } diff --git a/gui/src/duty_cycle.py b/gui/services/duty_cycle.py similarity index 100% rename from gui/src/duty_cycle.py rename to gui/services/duty_cycle.py diff --git a/gui/src/file_services.py b/gui/services/file_services.py similarity index 81% rename from gui/src/file_services.py rename to gui/services/file_services.py index 21c7940..5b29a7e 100644 --- a/gui/src/file_services.py +++ b/gui/services/file_services.py @@ -11,6 +11,7 @@ def __init__(self): pass + def get_projects_id(self, root_folder): projects_id = [] @@ -19,39 +20,37 @@ def get_projects_id(self, root_folder): # The relative regular expression (RegEx) is the following: r = re.compile("^[0-9]{1,2}-[0-9]{2}$") - # At first, check if a specific directory or mount point exists - print(os.path.exists(root_folder)) + if('home02' in root_folder): - if os.path.ismount(root_folder): - - print(f"Disk {root_folder} is mounted and visible.") + root_folder = '/home02/' - try: + if(root_folder=='/home02/'): + print('TEST folder') + root_folder = root_folder + 'fabio.schirru/Data/zanichelli/' + print('New test folder: ', root_folder) - tmp_subfolders = os.listdir(root_folder) + try: - except: + tmp_subfolders = os.listdir(root_folder) - pass - - else: + except: - for i in range(len(tmp_subfolders)): - # Check if the file is a an existing directory - if os.path.isdir(root_folder + tmp_subfolders[i]): - # Check if the folder matches the RegEx - if r.match(tmp_subfolders[i]) is not None: - - projects_id.append(tmp_subfolders[i]) + pass else: - - print(f"Disk {root_folder} is not mounted or not visible.") - + for i in range(len(tmp_subfolders)): + # Check if the file is a an existing directory + if os.path.isdir(root_folder + tmp_subfolders[i]): + # Check if the folder matches the RegEx + if r.match(tmp_subfolders[i]) is not None: + + projects_id.append(tmp_subfolders[i]) + return projects_id + def get_folders(self, folder, folder_level): # Each folder level related to a combo box has its own filtering rules to get the folders @@ -116,6 +115,7 @@ def get_folders(self, folder, folder_level): return folders + def ping_server(self, remote_ip): @@ -141,4 +141,4 @@ def ping_server(self, remote_ip): except Exception as e: print(f"Error pinging server: {e}") - return False \ No newline at end of file + return False diff --git a/requirements.txt b/requirements.txt index e5eed69..3348077 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,3 @@ -astropy==6.1.0 -astropy-iers-data==0.2024.5.6.0.29.28 -click==8.1.7 -contourpy==1.2.1 -cycler==0.12.1 -fonttools==4.51.0 -kiwisolver==1.4.5 -matplotlib==3.8.4 -numpy==1.26.4 -packaging==24.0 -pillow==10.3.0 -ptclassfiller==[gildas v. apr24a] -pyerfa==2.0.1.4 -pyparsing==3.1.2 -PyQt5==5.15.9 -pyqt5-plugins==5.15.9.2.3 -PyQt5-Qt5==5.15.2 -PyQt5-sip==12.12.1 -pyqt5-tools==5.15.9.3.3 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -PyYAML==6.0.1 -qt5-applications==5.15.2.2.3 -qt5-tools==5.15.2.1.3 -six==1.16.0 +numpy>=1.26,<2.0 +astropy>=6.1,<7.0 +PyQt5>=5.15,<6.0 diff --git a/src/discosscan.py b/src/discosscan.py index c7ec24c..b0fea4f 100644 --- a/src/discosscan.py +++ b/src/discosscan.py @@ -34,7 +34,6 @@ from pyclassfiller import code from .scancycle import ScanCycle -from .progress import Progress #SUMMARY = "summary.fits" @@ -248,6 +247,8 @@ def convert_cycle(self, index, current_cycle): # function called by "write_observation(self, scan_cycle, first_subscan_index):" def _load_metadata(self, section, polarization, index): + + print("_load_metadata", section, polarization, index) with fits.open(self.subscans[index][0]) as subscan: self.location = (subscan[0].header["SiteLongitude"] * u.rad, @@ -287,8 +288,18 @@ def _load_metadata(self, section, polarization, index): # To overcome this issue, it is defined a variable "self.section_current_val" which will be updated any time at the end of the function # So: during the first loop the variable is declared NULL and nothing happens since the section = 0. At the end of the loop the variable gets the value of the section i.e. 0 # In the second loop, the section value is still 0 which is the value of the variable. In this case the section value is augmentd by +1 and we can read data for section 1 - if(section == self.section_current_val): - section = section + 1 + + + # this is valid for SARDARA and SKARAB spectra and stokes types but NOT for stokes with XARCOS + + # print(subscan["SECTION TABLE"].data[0][1]) # to get the spectra type as "simple", "spectra", "stokes" + if not (self.summary['backendname'] == 'XAr'): + # section["type"] == "spectra": + if(section == self.section_current_val): + + section = section + 1 + + for sec in subscan["SECTION TABLE"].data: if sec["id"] == section: @@ -324,6 +335,8 @@ def _load_metadata(self, section, polarization, index): self.central_channel = (nchan / 2) + 1 self.offsetFrequencyAt0 = 0 + print("from metadata: ", section, polarization, index, self.bandwidth) + # Update section_current_value self.section_current_val = section @@ -340,6 +353,8 @@ def write_observation(self, scan_cycle, first_subscan_index): for sec_id, v in scan_cycle.data.items(): + print("v items", v.items()) + for pol, data in v.items(): # For Skarab case Nodding (one file per each feed), because of the current section Table structure we need to add +2 to the value of the sec_id @@ -406,7 +421,10 @@ def write_observation(self, scan_cycle, first_subscan_index): obs.head.gen.num = 0 obs.head.gen.ver = 0 # obs.head.gen.teles = self.antenna + " " + "Feed [" + str(self.feeds[sec_id]) + "]" old - obs.head.gen.teles = self.antenna + "-" + str(self.receiver) + "-" + self.summary['backendname'] + "-" + self.get_pol_type_string_converted(pol) + + + obs.head.gen.teles = self.antenna[0:3] + "-" + str(self.receiver) + "-" + self.summary['backendname'] + "-" + self.get_pol_type_string_converted(pol) + print(obs.head.gen.teles) obs.head.gen.dobs = int(self.observation_time.mjd) - 60549 obs.head.gen.dred = int(self.record_time.mjd) - 60549 obs.head.gen.typec = code.coord.equ @@ -478,6 +496,7 @@ def write_observation(self, scan_cycle, first_subscan_index): #obs.head.spe.line = "SEC%d-%s" % (sec_id, pol_converted) # old #obs.head.spe.line = "F%s-%s" % (str(self.feeds[sec_id]), str(self.bandwidth)) + " " + str(sec_id) obs.head.spe.line = "F%s-%s" % (str(self.feeds[sec_id]), str(self.bandwidth)) + print(obs.head.spe.line) # Check if data are relative to type=spectra # If so check the number of feeds used: if n > 1 then nodding mode is applied @@ -552,8 +571,14 @@ def write_observation(self, scan_cycle, first_subscan_index): logger.debug("skip calibration") obs.head.gen.tsys = 1. # ANTENNA TEMP TABLE is unknown + print('***Skip Calibration***') + print(on[900:910]) + print(off[900:910]) + obs.datay = ((on - off) / off ) + print(obs.datay[900:910]) + # Class needs numbers with some decimal digits # 0. is therefore not allowed and would a blanck histo bar in the graph # 0. must be converted for example to 0.00000000001 as declaring the single array lelemnts as 0.0000 won't work diff --git a/src/scancycle.py b/src/scancycle.py index 8ae6d4c..1c452e7 100644 --- a/src/scancycle.py +++ b/src/scancycle.py @@ -3,7 +3,10 @@ import numpy as np -POLARIZATIONS = ["LCP", "RCP"] +#POLARIZATIONS = ["LCP", "RCP"] +#POLARIZATIONS = ["LL", "RR", "LR", "RL"] +POLARIZATIONS = ["LCP", "RCP", "Q", "U"] + POLARIZATIONS_SPECTRA = ["LCP", "RCP"] class ScanCycle(object): @@ -63,6 +66,7 @@ def __init__(self, sections, duty_cycle, backend_name, current_cycle): ('samples', np.int_), ('integration', np.float64)]) print(self.data.items()) + @property def sections(self):