From 2f0977f2a9bfa253c56e3776c4ee2def99e50536 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Mon, 14 Oct 2024 12:39:20 -0400 Subject: [PATCH 01/20] add __version__ --- aperoll/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aperoll/__init__.py b/aperoll/__init__.py index e69de29..f103005 100644 --- a/aperoll/__init__.py +++ b/aperoll/__init__.py @@ -0,0 +1,3 @@ +import ska_helpers + +__version__ = ska_helpers.get_version(__package__) From 52cd939faeed4afe87889037dab8215e91d39001 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 11 Oct 2024 13:44:50 -0400 Subject: [PATCH 02/20] add run-sparkles button and remove test button --- aperoll/widgets/main_window.py | 136 +++++++++++++++------------------ aperoll/widgets/parameters.py | 8 +- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index d8262df..c812bcc 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -1,8 +1,10 @@ # from PyQt5 import QtCore as QtC, QtWidgets as QtW, QtGui as QtG from pathlib import Path +from pprint import pprint from tempfile import TemporaryDirectory import PyQt5.QtWebEngineWidgets as QtWe +import sparkles from astropy import units as u from cxotime import CxoTime from proseco import get_aca_catalog @@ -53,7 +55,8 @@ def __init__(self, opts=None): layout.setStretch(1, 4) self.setLayout(layout) - self.parameters.do_it.connect(self._do_it) + self.parameters.do_it.connect(self._run_proseco) + self.parameters.run_sparkles.connect(self._run_sparkles) self.parameters.draw_test.connect(self._draw_test) self.plot.attitude_changed.connect(self.parameters.set_ra_dec) @@ -71,7 +74,6 @@ def closeEvent(self, event): event.accept() def _init(self): - print("parameters:", self.parameters.values) if self.parameters.values: # obsid = self.parameters.values["obsid"] ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] @@ -86,7 +88,7 @@ def _init(self): aca_attitude = Quat( equatorial=(float(ra / u.deg), float(dec / u.deg), roll) ) - print("ra, dec, roll =", (float(ra / u.deg), float(dec / u.deg), roll)) + # print("ra, dec, roll =", (float(ra / u.deg), float(dec / u.deg), roll)) self.plot.set_base_attitude(aca_attitude, update=False) self.plot.set_time(time, update=True) @@ -103,78 +105,66 @@ def _draw_test(self): ra_offset=dq.ra, dec_offset=dq.dec, roll_offset=dq.roll ) - def _do_it(self): - print("parameters:", self.parameters.values) + def _proseco_args(self): + obsid = self.parameters.values["obsid"] + ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] + roll = self.parameters.values["roll"] + time = CxoTime(self.parameters.values["date"]) + + aca_attitude = Quat(equatorial=(float(ra / u.deg), float(dec / u.deg), roll)) + + args = { + "obsid": obsid, + "att": aca_attitude, + "date": time, + "n_fid": self.parameters.values["n_fid"], + "n_guide": self.parameters.values["n_guide"], + "dither_acq": (16, 16), # standard dither with ACIS + "dither_guide": (16, 16), # standard dither with ACIS + "t_ccd_acq": self.parameters.values["t_ccd"], + "t_ccd_guide": self.parameters.values["t_ccd"], + "man_angle": 0, # what is a sensible number to use?? + "detector": self.parameters.values["instrument"], + "sim_offset": 0, # docs say this is optional, but it does not seem to be + "focus_offset": 0, # docs say this is optional, but it does not seem to be + } + return args + + def _run_proseco(self): + # print("parameters:", self.parameters.values) if self.parameters.values: - obsid = self.parameters.values["obsid"] - ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] - roll = self.parameters.values["roll"] - time = CxoTime(self.parameters.values["date"]) + args = self._proseco_args() + pprint(args) + catalog = get_aca_catalog(**args) + self.plot.set_catalog(catalog, update=False) - # aca_attitude = calc_aca_from_targ( - # Quat(equatorial=(float(ra / u.deg), float(dec / u.deg), nominal_roll)), - # 0, - # 0 - # ) - aca_attitude = Quat( - equatorial=(float(ra / u.deg), float(dec / u.deg), roll) - ) - print("ra, dec, roll =", (float(ra / u.deg), float(dec / u.deg), roll)) - from pprint import pprint - - pprint( - { - "obsid": obsid, - "att": aca_attitude, - "date": time, - "n_fid": self.parameters.values["n_fid"], - "n_guide": self.parameters.values["n_guide"], - "dither_acq": (16, 16), # standard dither with ACIS - "dither_guide": (16, 16), # standard dither with ACIS - "t_ccd_acq": self.parameters.values["t_ccd"], - "t_ccd_guide": self.parameters.values["t_ccd"], - "man_angle": 0, # what is a sensible number to use?? - "detector": self.parameters.values["instrument"], - "sim_offset": 0, # docs say this is optional, but it does not seem to be - "focus_offset": 0, # docs say this is optional, but it does not seem to be - } - ) - catalog = get_aca_catalog( - obsid=obsid, - att=aca_attitude, - date=time, - n_fid=self.parameters.values["n_fid"], - n_guide=self.parameters.values["n_guide"], - dither_acq=(16, 16), # standard dither with ACIS - dither_guide=(16, 16), # standard dither with ACIS - t_ccd_acq=self.parameters.values["t_ccd"], - t_ccd_guide=self.parameters.values["t_ccd"], - man_angle=0, # what is a sensible number to use?? - detector=self.parameters.values["instrument"], - sim_offset=0, # docs say this is optional, but it does not seem to be - focus_offset=0, # docs say this is optional, but it does not seem to be + def _run_sparkles(self): + # print("parameters:", self.parameters.values) + if self.parameters.values: + args = self._proseco_args() + pprint(args) + catalog = get_aca_catalog(**args) + + sparkles.run_aca_review( + "Exploration", + acars=[catalog.get_review_table()], + report_dir=self._dir / "sparkles", + report_level="all", + roll_level="none", ) - - # run_aca_review( - # 'Exploration', - # acars=[catalog.get_review_table()], - # report_dir=self._dir / 'sparkles', - # report_level='all', - # roll_level='none', - # ) - # print(f'sparkles report at {self._dir / "sparkles"}') - # try: - # w = QtW.QMainWindow(self) - # w.resize(1400, 1000) - # web = QtWe.QWebEngineView(w) - # w.setCentralWidget(web) - # self.web_page = WebPage() - # web.setPage(self.web_page) - # url = self._dir / 'sparkles' / 'index.html' - # web.load(QtC.QUrl(f'file://{url}')) - # web.show() - # w.show() - # except Exception as e: - # print(e) + print(f"sparkles report at {self._dir / 'sparkles'}") + try: + w = QtW.QMainWindow(self) + w.resize(1400, 1000) + web = QtWe.QWebEngineView(w) + w.setCentralWidget(web) + self.web_page = WebPage() + web.setPage(self.web_page) + url = self._dir / "sparkles" / "index.html" + web.load(QtC.QUrl(f"file://{url}")) + web.show() + w.show() + except Exception as e: + print(e) self.plot.set_catalog(catalog, update=False) diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 2b76c72..51a92d8 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -33,6 +33,7 @@ def get_parameters(): class Parameters(QtW.QWidget): do_it = QtC.pyqtSignal() + run_sparkles = QtC.pyqtSignal() draw_test = QtC.pyqtSignal() def __init__(self, **kwargs): # noqa: PLR0915 @@ -56,7 +57,8 @@ def __init__(self, **kwargs): # noqa: PLR0915 self.instrument_label = QtW.QLabel("instrument") self.instrument_edit = QtW.QComboBox(self) self.instrument_edit.addItems(["ACIS-S", "ACIS-I", "HRC-S", "HRC-I"]) - self.do = QtW.QPushButton("Starcheck") + self.do = QtW.QPushButton("Get Catalog") + self.run_sparkles_button = QtW.QPushButton("Run Sparkles") self.draw_test_button = QtW.QPushButton("Draw Test") v_layout = QtW.QVBoxLayout(self) layout = QtW.QGridLayout() @@ -77,7 +79,8 @@ def __init__(self, **kwargs): # noqa: PLR0915 layout.addWidget(self.instrument_label, 7, 0) layout.addWidget(self.instrument_edit, 7, 1) layout.addWidget(self.do, 8, 1) - layout.addWidget(self.draw_test_button, 9, 1) + layout.addWidget(self.run_sparkles_button, 9, 1) + # layout.addWidget(self.draw_test_button, 10, 1) v_layout.addLayout(layout) v_layout.addStretch(0) @@ -99,6 +102,7 @@ def __init__(self, **kwargs): # noqa: PLR0915 self.values = self._validate() self.do.clicked.connect(self._do_it) self.draw_test_button.clicked.connect(self._draw_test) + self.run_sparkles_button.clicked.connect(self.run_sparkles) def _draw_test(self): self.values = self._validate() From 52b8a5da30ca9d972092c41c2d4943f91e916ca8 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 11 Oct 2024 14:49:28 -0400 Subject: [PATCH 03/20] read from file --- aperoll/scripts/aperoll_main.py | 7 +++-- aperoll/widgets/main_window.py | 2 ++ aperoll/widgets/parameters.py | 49 ++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/aperoll/scripts/aperoll_main.py b/aperoll/scripts/aperoll_main.py index 885dfce..b887596 100755 --- a/aperoll/scripts/aperoll_main.py +++ b/aperoll/scripts/aperoll_main.py @@ -10,9 +10,10 @@ def get_parser(): import argparse parse = argparse.ArgumentParser() - parse.add_argument("--date", default="2021-08-23 00:00:00.000") - parse.add_argument("--ra", default="6.45483333") - parse.add_argument("--dec", default="-26.03683611") + parse.add_argument("file", nargs="?", default=None) + parse.add_argument("--date") + parse.add_argument("--ra") + parse.add_argument("--dec") return parse diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index c812bcc..e63b6e3 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -39,6 +39,8 @@ def __init__(self, opts=None): opts = {} if opts is None else opts opts = {k: opts[k] for k in opts if opts[k] is not None} + pprint(opts) + self.web_page = None self._tmp_dir = TemporaryDirectory() diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 51a92d8..1895a79 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -1,3 +1,6 @@ +import json +from pprint import pprint + import Ska.Sun as sun from astropy import units as u from cxotime.cxotime import CxoTime @@ -37,8 +40,6 @@ class Parameters(QtW.QWidget): draw_test = QtC.pyqtSignal() def __init__(self, **kwargs): # noqa: PLR0915 - kwargs = {} - super().__init__() self.date_label = QtW.QLabel("date") self.date_edit = QtW.QLineEdit(self) @@ -84,14 +85,48 @@ def __init__(self, **kwargs): # noqa: PLR0915 v_layout.addLayout(layout) v_layout.addStretch(0) + file = kwargs.pop("file", None) + if file: + with open(file) as fh: + file_kwargs = json.load(fh)[0] # assuming there is only one entry + + file_kwargs["date"] = file_kwargs["obs_date"] + file_kwargs["ra"] = file_kwargs["ra_targ"] + file_kwargs["dec"] = file_kwargs["dec_targ"] + file_kwargs["roll"] = file_kwargs["roll_targ"] + file_kwargs["dither"] = ( + file_kwargs["dither_y"], + file_kwargs["dither_z"], + ) + file_kwargs["instrument"] = file_kwargs["detector"] + for key in [ + "obs_date", + "ra_targ", + "dec_targ", + "roll_targ", + "dither_y", + "dither_z", + "detector", + ]: + del file_kwargs[key] + kwargs.update(file_kwargs) + params = get_parameters() + if abs(kwargs.get("obsid", 0)) < 38000: + kwargs["n_fid"] = "3" + kwargs["n_guide"] = "5" + else: + kwargs["n_fid"] = "0" + kwargs["n_guide"] = "8" + + pprint(kwargs) self.date_edit.setText(kwargs.get("date", params["date"])) - self.ra_edit.setText(kwargs.get("ra", f"{params['attitude'].ra:.8f}")) - self.dec_edit.setText(kwargs.get("dec", f"{params['attitude'].dec:.8f}")) - self.roll_edit.setText(kwargs.get("roll", f"{params['attitude'].roll:.8f}")) - self.n_guide_edit.setText(kwargs.get("n_guide", "5")) - self.n_fid_edit.setText(kwargs.get("n_fid", "3")) + self.ra_edit.setText(f"{kwargs.get('ra', params['attitude'].ra):.8f}") + self.dec_edit.setText(f"{kwargs.get('dec', params['attitude'].dec):.8f}") + self.roll_edit.setText(f"{kwargs.get('roll', params['attitude'].roll):.8f}") + self.n_guide_edit.setText(f"{kwargs['n_guide']}") + self.n_fid_edit.setText(f"{kwargs['n_fid']}") self.n_t_ccd_edit.setText(kwargs.get("t_ccd", f"{params['t_ccd']:.2f}")) self.instrument_edit.setCurrentText( kwargs.get("instrument", params["instrument"]) From 8ba7497f52edd4c43eacda90bfd6ed9571a32e53 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 11 Oct 2024 16:04:30 -0400 Subject: [PATCH 04/20] show star catalog --- aperoll/widgets/main_window.py | 111 ++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 9 deletions(-) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index e63b6e3..8d334ce 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -3,18 +3,82 @@ from pprint import pprint from tempfile import TemporaryDirectory +import PyQt5.QtGui as QtG import PyQt5.QtWebEngineWidgets as QtWe +import PyQt5.QtWidgets as QtW import sparkles from astropy import units as u from cxotime import CxoTime from proseco import get_aca_catalog from PyQt5 import QtCore as QtC -from PyQt5 import QtWidgets as QtW from Quaternion import Quat from .parameters import Parameters from .star_plot import StarPlot +STYLE = """ + +""" + class WebPage(QtWe.QWebEnginePage): def __init__(self, parent=None): @@ -48,12 +112,19 @@ def __init__(self, opts=None): self.plot = StarPlot() self.parameters = Parameters(**opts) + self.textEdit = QtW.QTextEdit() + font = QtG.QFont("Courier New") # setting a fixed-width font (close enough) + font.setPixelSize(5) # setting a pixel size so it can be changed later + self.textEdit.setFont(font) layout = QtW.QHBoxLayout(self) - layout.addWidget(self.parameters) + layout_2 = QtW.QVBoxLayout(self) + layout_2.addWidget(self.parameters) + layout_2.addWidget(self.textEdit) + layout.addLayout(layout_2) layout.addWidget(self.plot) - layout.setStretch(0, 1) + layout.setStretch(0, 3) layout.setStretch(1, 4) self.setLayout(layout) @@ -63,11 +134,6 @@ def __init__(self, opts=None): self.plot.attitude_changed.connect(self.parameters.set_ra_dec) self._init() - # try: - # self._do_it() - # except Exception as e: - # print(e) - # pass def closeEvent(self, event): if self.web_page is not None: @@ -133,13 +199,40 @@ def _proseco_args(self): return args def _run_proseco(self): - # print("parameters:", self.parameters.values) + print("parameters:", self.parameters.values) if self.parameters.values: args = self._proseco_args() pprint(args) catalog = get_aca_catalog(**args) self.plot.set_catalog(catalog, update=False) + aca = catalog.get_review_table() + + sparkles.core.check_catalog(aca) + + # aca.messages + + # self.textEdit.setText(table_to_html(catalog)) + self.textEdit.setText(f"{STYLE}
{aca.get_text_pre()}
") + + def resizeEvent(self, _size): + font = self.textEdit.font() + header = ( + "idx slot id type sz p_acq mag mag_err " + "maxmag yang zang row col dim res halfw" + ) + n_lines = 35 + scale_x = float(0.9 * self.textEdit.width()) / QtG.QFontMetrics(font).width( + header + ) + scale_y = float(0.9 * self.textEdit.height()) / ( + n_lines * QtG.QFontMetrics(font).height() + ) + pix_size = int(font.pixelSize() * min(scale_x, scale_y)) + if pix_size > 0: + font.setPixelSize(pix_size) + self.textEdit.setFont(font) + def _run_sparkles(self): # print("parameters:", self.parameters.values) if self.parameters.values: From 697471406096fd6f483bde7c05b917fe04144e87 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sat, 12 Oct 2024 11:38:05 -0400 Subject: [PATCH 05/20] show catalog idx --- aperoll/widgets/star_plot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 7e7d2bf..cd304da 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -435,6 +435,14 @@ def show_catalog(self): fids = cat[cat["type"] == "FID"] mon_wins = cat[cat["type"] == "MON"] + for star in cat: + txt = self.scene.addText( + f"{star['idx']}", + QtG.QFont("Arial", 32), + ) + txt.setPos(star["row"] + 16, -star["col"] - 40) + txt.setDefaultTextColor(QtG.QColor("red")) + self._catalog_items.append(txt) for gui_star in gui_stars: w = 20 # note that the coordinate system is (row, -col) From e97d3b1f93b705fc6c6490d18e9a2c18069d428c Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sat, 12 Oct 2024 12:41:19 -0400 Subject: [PATCH 06/20] add Star item --- aperoll/widgets/star_plot.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index cd304da..5159a78 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -44,6 +44,19 @@ def get_stars(starcat_time, quaternion, radius=2): return stars +class Star(QtW.QGraphicsEllipseItem): + def __init__(self, star, parent=None, highlight=False): + s = symsize(star["MAG_ACA"]) + # note that the coordinate system is (row, -col) + rect = QtC.QRectF(star["row"] - s / 2, -star["col"] - s / 2, s, s) + super().__init__(rect, parent) + self.star = star + color = "red" if highlight else "black" + self.setBrush(QtG.QBrush(QtG.QColor(color))) + + def __repr__(self): + return f"Star({self.star['AGASC_ID']})" + class StarView(QtW.QGraphicsView): roll_changed = QtC.pyqtSignal(float) @@ -268,7 +281,7 @@ def __init__(self, parent=None): # "current attitude" refers to the attitude taking into account the viewport's position self._current_attitude = None self._time = None - self._highlight = None + self._highlight = [] self._catalog = None self._catalog_items = [] @@ -401,17 +414,10 @@ def show_stars(self): ) black_pen = QtG.QPen() black_pen.setWidth(2) - black_brush = QtG.QBrush(QtG.QColor("black")) - red_pen = QtG.QPen(QtG.QColor("red")) - red_brush = QtG.QBrush(QtG.QColor("red")) for star in self.stars: - s = symsize(star["MAG_ACA"]) - # note that the coordinate system is (row, -col) - rect = QtC.QRectF(star["row"] - s / 2, -star["col"] - s / 2, s, s) - if self._highlight is not None and star["AGASC_ID"] in self._highlight: - self.scene.addEllipse(rect, red_pen, red_brush) - else: - self.scene.addEllipse(rect, black_pen, black_brush) + self.scene.addItem( + Star(star, highlight=star["AGASC_ID"] in self._highlight) + ) # self.view.centerOn(QtC.QPointF(self._origin[0], self._origin[1])) self.view.re_center() From 4e45e83cd439debc0babc161923768dfcae044cd Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sun, 13 Oct 2024 09:41:42 -0400 Subject: [PATCH 07/20] set color of bad stars --- aperoll/widgets/star_plot.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 5159a78..cbab138 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -51,12 +51,42 @@ def __init__(self, star, parent=None, highlight=False): rect = QtC.QRectF(star["row"] - s / 2, -star["col"] - s / 2, s, s) super().__init__(rect, parent) self.star = star - color = "red" if highlight else "black" - self.setBrush(QtG.QBrush(QtG.QColor(color))) + self.highlight = highlight + color = self.color() + self.setBrush(QtG.QBrush(color)) + self.setPen(QtG.QPen(color)) def __repr__(self): return f"Star({self.star['AGASC_ID']})" + def color(self): + if self.highlight: + return QtG.QColor("red") + if self.star["MAG_ACA"] > 10.5: + return QtG.QColor("lightGray") + if self.bad(): + return QtG.QColor(255, 99, 71, 191) + return QtG.QColor("black") + + def bad(self): + ok = ( + (self.star["CLASS"] == 0) + & (self.star["MAG_ACA"] > 5.3) + & (self.star["MAG_ACA"] < 11.0) + & (~np.isclose(self.star["COLOR1"], 0.7)) + & (self.star["MAG_ACA_ERR"] < 100.0) # mag_err is in 0.01 mag + & (self.star["ASPQ1"] < 40) + & ( # Less than 2 arcsec centroid offset due to nearby spoiler + self.star["ASPQ2"] == 0 + ) + & (self.star["POS_ERR"] < 3000) # Position error < 3.0 arcsec + & ( + (self.star["VAR"] == -9999) | (self.star["VAR"] == 5) + ) # Not known to vary > 0.2 mag + ) + return not ok + + class StarView(QtW.QGraphicsView): roll_changed = QtC.pyqtSignal(float) From 22ab6ae2f246a1a1d418936a0647ca243c9bf9b4 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sun, 13 Oct 2024 09:42:20 -0400 Subject: [PATCH 08/20] fix warning --- aperoll/widgets/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 8d334ce..fc199cf 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -118,7 +118,7 @@ def __init__(self, opts=None): self.textEdit.setFont(font) layout = QtW.QHBoxLayout(self) - layout_2 = QtW.QVBoxLayout(self) + layout_2 = QtW.QVBoxLayout() layout_2.addWidget(self.parameters) layout_2.addWidget(self.textEdit) layout.addLayout(layout_2) From e80c7dbe52c4aaea8edd885667a0349967cce220 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sun, 13 Oct 2024 09:51:14 -0400 Subject: [PATCH 09/20] add tooltip --- aperoll/widgets/star_plot.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index cbab138..5b7f689 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -86,12 +86,28 @@ def bad(self): ) return not ok + def text(self): + return ( + "
"
+            f"ID:      {self.star['AGASC_ID']}\n"
+            f"mag:     {self.star['MAG_ACA']:.2f} +- {self.star['MAG_ACA_ERR']/100:.2}\n"
+            f"color:   {self.star['COLOR1']:.2f}\n"
+            f"ASPQ1:   {self.star['ASPQ1']}\n"
+            f"ASPQ2:   {self.star['ASPQ2']}\n"
+            f"class:   {self.star['CLASS']}\n"
+            f"pos err: {self.star['POS_ERR']/1000} mas\n"
+            f"VAR:     {self.star['VAR']}"
+            "
" + ) + class StarView(QtW.QGraphicsView): roll_changed = QtC.pyqtSignal(float) def __init__(self, scene=None): super().__init__(scene) + # mouseTracking is set so we can show tooltips + self.setMouseTracking(True) self._start = None self._rotating = False @@ -99,6 +115,15 @@ def __init__(self, scene=None): def mouseMoveEvent(self, event): pos = event.pos() + + items = [item for item in self.items(event.pos()) if isinstance(item, Star)] + if items: + global_pos = event.globalPos() + # supposedly, the following should cause the tooltip to stay for a long time + # but it is the same + # QtW.QToolTip.showText(global_pos, items[0].text(), self, QtC.QRect(), 1000000000) + QtW.QToolTip.showText(global_pos, items[0].text()) + if self._start is None: return From 8102b71663311098a0948cec26aa6be5b03c3d65 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sun, 13 Oct 2024 12:12:47 -0400 Subject: [PATCH 10/20] include/exclude stars --- aperoll/widgets/main_window.py | 13 +++++++ aperoll/widgets/parameters.py | 63 +++++++++++++++++++++++++++++++++- aperoll/widgets/star_plot.py | 55 ++++++++++++++++++++++++++--- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index fc199cf..684334c 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -128,6 +128,9 @@ def __init__(self, opts=None): layout.setStretch(1, 4) self.setLayout(layout) + self.plot.include_star.connect(self.parameters.include_star) + # self.plot.exclude_star.connect(self.parameters.exclude_star) + self.parameters.do_it.connect(self._run_proseco) self.parameters.run_sparkles.connect(self._run_sparkles) self.parameters.draw_test.connect(self._draw_test) @@ -196,6 +199,16 @@ def _proseco_args(self): "sim_offset": 0, # docs say this is optional, but it does not seem to be "focus_offset": 0, # docs say this is optional, but it does not seem to be } + + for key in [ + "exclude_ids_guide", + "include_ids_guide", + "exclude_ids_acq", + "include_ids_acq", + ]: + if self.parameters.values[key]: + args[key] = self.parameters.values[key] + return args def _run_proseco(self): diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 1895a79..0846b18 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -61,6 +61,14 @@ def __init__(self, **kwargs): # noqa: PLR0915 self.do = QtW.QPushButton("Get Catalog") self.run_sparkles_button = QtW.QPushButton("Run Sparkles") self.draw_test_button = QtW.QPushButton("Draw Test") + self.include = { + "acq": QtW.QListWidget(self), + "guide": QtW.QListWidget(self), + } + self.exclude = { + "acq": QtW.QListWidget(self), + "guide": QtW.QListWidget(self), + } v_layout = QtW.QVBoxLayout(self) layout = QtW.QGridLayout() layout.addWidget(self.date_label, 0, 0) @@ -81,7 +89,15 @@ def __init__(self, **kwargs): # noqa: PLR0915 layout.addWidget(self.instrument_edit, 7, 1) layout.addWidget(self.do, 8, 1) layout.addWidget(self.run_sparkles_button, 9, 1) - # layout.addWidget(self.draw_test_button, 10, 1) + + tab = QtW.QTabWidget() + tab.addTab(self.include["acq"], "Include Acq.") + tab.addTab(self.exclude["acq"], "Exclude Acq.") + tab.addTab(self.include["guide"], "Include Guide") + tab.addTab(self.exclude["guide"], "Exclude Guide") + tab.setCurrentIndex(0) + layout.addWidget(tab, 10, 0, 1, 2) + v_layout.addLayout(layout) v_layout.addStretch(0) @@ -170,6 +186,22 @@ def _validate(self, quiet=False): "t_ccd": float(self.n_t_ccd_edit.text()), "instrument": self.instrument_edit.currentText(), "obsid": obsid, + "exclude_ids_acq": [ + int(self.exclude["acq"].item(i).text()) + for i in range(self.exclude["acq"].count()) + ], + "include_ids_acq": [ + int(self.include["acq"].item(i).text()) + for i in range(self.include["acq"].count()) + ], + "exclude_ids_guide": [ + int(self.exclude["guide"].item(i).text()) + for i in range(self.exclude["guide"].count()) + ], + "include_ids_guide": [ + int(self.include["guide"].item(i).text()) + for i in range(self.include["guide"].count()) + ], } except Exception as e: if not quiet: @@ -185,3 +217,32 @@ def set_ra_dec(self, ra, dec, roll): self.ra_edit.setText(f"{ra:.8f}") self.dec_edit.setText(f"{dec:.8f}") self.roll_edit.setText(f"{roll:.8f}") + + def include_star(self, star, type, include): + if include is True: + self._include_star(star, type, True) + self._exclude_star(star, type, False) + elif include is False: + self._include_star(star, type, False) + self._exclude_star(star, type, True) + else: + self._include_star(star, type, include=False) + self._exclude_star(star, type, exclude=False) + + def _include_star(self, star, type, include): + items = self.include[type].findItems(f"{star}", QtC.Qt.MatchExactly) + if include: + if not items: + self.include[type].addItem(f"{star}") + else: + for it in items: + self.include[type].takeItem(self.include[type].row(it)) + + def _exclude_star(self, star, type, exclude): + items = self.exclude[type].findItems(f"{star}", QtC.Qt.MatchExactly) + if exclude: + if not items: + self.exclude[type].addItem(f"{star}") + else: + for it in items: + self.exclude[type].takeItem(self.exclude[type].row(it)) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 5b7f689..8b8c166 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -55,6 +55,10 @@ def __init__(self, star, parent=None, highlight=False): color = self.color() self.setBrush(QtG.QBrush(color)) self.setPen(QtG.QPen(color)) + self.included = { + "acq": None, + "guide": None, + } def __repr__(self): return f"Star({self.star['AGASC_ID']})" @@ -103,6 +107,7 @@ def text(self): class StarView(QtW.QGraphicsView): roll_changed = QtC.pyqtSignal(float) + include_star = QtC.pyqtSignal(int, str, object) def __init__(self, scene=None): super().__init__(scene) @@ -163,12 +168,14 @@ def mouseMoveEvent(self, event): self._start = pos def mouseReleaseEvent(self, event): - self._start = None + if event.button() == QtC.Qt.LeftButton: + self._start = None def mousePressEvent(self, event): - self._moving = False - self._rotating = False - self._start = event.pos() + if event.button() == QtC.Qt.LeftButton: + self._moving = False + self._rotating = False + self._start = event.pos() def wheelEvent(self, event): scale = 1 + 0.5 * event.angleDelta().y() / 360 @@ -308,9 +315,47 @@ def re_center(self): transform = self.viewportTransform().rotate(-self.get_roll_offset()) self.setTransform(transform) + def contextMenuEvent(self, event): + items = [item for item in self.items(event.pos()) if isinstance(item, Star)] + if not items: + return + item = items[0] + + menu = QtW.QMenu() + + incl_action = QtW.QAction("include acq", menu, checkable=True) + incl_action.setChecked(item.included["acq"] is True) + menu.addAction(incl_action) + + excl_action = QtW.QAction("exclude acq", menu, checkable=True) + excl_action.setChecked(item.included["acq"] is False) + menu.addAction(excl_action) + + incl_action = QtW.QAction("include guide", menu, checkable=True) + incl_action.setChecked(item.included["guide"] is True) + menu.addAction(incl_action) + + excl_action = QtW.QAction("exclude guide", menu, checkable=True) + excl_action.setChecked(item.included["guide"] is False) + menu.addAction(excl_action) + + result = menu.exec_(event.globalPos()) + if result is not None: + action, action_type = result.text().split() + if items: + if action == "include": + item.included[action_type] = True if result.isChecked() else None + elif action == "exclude": + item.included[action_type] = False if result.isChecked() else None + self.include_star.emit( + item.star["AGASC_ID"], action_type, item.included[action_type] + ) + event.accept() + class StarPlot(QtW.QWidget): attitude_changed = QtC.pyqtSignal(float, float, float) + include_star = QtC.pyqtSignal(int, str, object) def __init__(self, parent=None): super().__init__(parent) @@ -345,6 +390,8 @@ def __init__(self, parent=None): self.scene.sceneRectChanged.connect(self._radec_changed) self.view.roll_changed.connect(self._roll_changed) + self.view.include_star.connect(self.include_star) + def _radec_changed(self): # RA/dec change when the scene rectangle changes, and its given by the rectangle's center # the base attitude corresponds to RA/dec at the origin, se we take the displacement From f686e6318a5c5ab53003e38bff8e0b66a6b138ca Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Sun, 13 Oct 2024 17:47:54 -0400 Subject: [PATCH 11/20] Add more parameters and improve the dialog a bit --- aperoll/widgets/main_window.py | 19 ++--- aperoll/widgets/parameters.py | 138 +++++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 42 deletions(-) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 684334c..414dba0 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -117,15 +117,16 @@ def __init__(self, opts=None): font.setPixelSize(5) # setting a pixel size so it can be changed later self.textEdit.setFont(font) - layout = QtW.QHBoxLayout(self) - layout_2 = QtW.QVBoxLayout() - layout_2.addWidget(self.parameters) + layout = QtW.QVBoxLayout(self) + layout_2 = QtW.QHBoxLayout() + + layout.addWidget(self.parameters) layout_2.addWidget(self.textEdit) + layout_2.addWidget(self.plot) layout.addLayout(layout_2) - layout.addWidget(self.plot) - layout.setStretch(0, 3) - layout.setStretch(1, 4) + layout.setStretch(0, 1) # the dialog on top should not stretch much + layout.setStretch(1, 10) self.setLayout(layout) self.plot.include_star.connect(self.parameters.include_star) @@ -190,11 +191,11 @@ def _proseco_args(self): "date": time, "n_fid": self.parameters.values["n_fid"], "n_guide": self.parameters.values["n_guide"], - "dither_acq": (16, 16), # standard dither with ACIS - "dither_guide": (16, 16), # standard dither with ACIS + "dither_acq": self.parameters.values["dither_acq"], + "dither_guide": self.parameters.values["dither_guide"], "t_ccd_acq": self.parameters.values["t_ccd"], "t_ccd_guide": self.parameters.values["t_ccd"], - "man_angle": 0, # what is a sensible number to use?? + "man_angle": self.parameters.values["man_angle"], "detector": self.parameters.values["instrument"], "sim_offset": 0, # docs say this is optional, but it does not seem to be "focus_offset": 0, # docs say this is optional, but it does not seem to be diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 0846b18..940d9a5 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -41,21 +41,15 @@ class Parameters(QtW.QWidget): def __init__(self, **kwargs): # noqa: PLR0915 super().__init__() - self.date_label = QtW.QLabel("date") self.date_edit = QtW.QLineEdit(self) - self.ra_label = QtW.QLabel("ra") + self.obsid_edit = QtW.QLineEdit(self) self.ra_edit = QtW.QLineEdit(self) - self.dec_label = QtW.QLabel("dec") self.dec_edit = QtW.QLineEdit(self) - self.roll_label = QtW.QLabel("roll") self.roll_edit = QtW.QLineEdit(self) - self.n_guide_label = QtW.QLabel("n_guide") self.n_guide_edit = QtW.QLineEdit(self) - self.n_fid_label = QtW.QLabel("n_fid") self.n_fid_edit = QtW.QLineEdit(self) - self.n_t_ccd_label = QtW.QLabel("t_ccd") self.n_t_ccd_edit = QtW.QLineEdit(self) - self.instrument_label = QtW.QLabel("instrument") + self.man_angle_edit = QtW.QLineEdit(self) self.instrument_edit = QtW.QComboBox(self) self.instrument_edit.addItems(["ACIS-S", "ACIS-I", "HRC-S", "HRC-I"]) self.do = QtW.QPushButton("Get Catalog") @@ -69,26 +63,74 @@ def __init__(self, **kwargs): # noqa: PLR0915 "acq": QtW.QListWidget(self), "guide": QtW.QListWidget(self), } - v_layout = QtW.QVBoxLayout(self) - layout = QtW.QGridLayout() - layout.addWidget(self.date_label, 0, 0) - layout.addWidget(self.date_edit, 0, 1) - layout.addWidget(self.ra_label, 1, 0) - layout.addWidget(self.ra_edit, 1, 1) - layout.addWidget(self.dec_label, 2, 0) - layout.addWidget(self.dec_edit, 2, 1) - layout.addWidget(self.roll_label, 3, 0) - layout.addWidget(self.roll_edit, 3, 1) - layout.addWidget(self.n_guide_label, 4, 0) - layout.addWidget(self.n_guide_edit, 4, 1) - layout.addWidget(self.n_fid_label, 5, 0) - layout.addWidget(self.n_fid_edit, 5, 1) - layout.addWidget(self.n_t_ccd_label, 6, 0) - layout.addWidget(self.n_t_ccd_edit, 6, 1) - layout.addWidget(self.instrument_label, 7, 0) - layout.addWidget(self.instrument_edit, 7, 1) - layout.addWidget(self.do, 8, 1) - layout.addWidget(self.run_sparkles_button, 9, 1) + self.dither_acq_y_edit = QtW.QLineEdit(self) + self.dither_acq_z_edit = QtW.QLineEdit(self) + self.dither_guide_y_edit = QtW.QLineEdit(self) + self.dither_guide_z_edit = QtW.QLineEdit(self) + + layout = QtW.QHBoxLayout() + + info_group_box = QtW.QGroupBox() + info_group_box_layout = QtW.QGridLayout() + info_group_box_layout.addWidget(QtW.QLabel("OBSID"), 0, 0, 1, 1) + info_group_box_layout.addWidget(self.obsid_edit, 0, 1, 1, 2) + info_group_box_layout.addWidget(QtW.QLabel("date"), 1, 0, 1, 1) + info_group_box_layout.addWidget(self.date_edit, 1, 1, 1, 2) + info_group_box_layout.addWidget(QtW.QLabel("instrument"), 2, 0, 1, 1) + info_group_box_layout.addWidget(self.instrument_edit, 2, 1, 1, 2) + for i in range(3): + info_group_box_layout.setColumnStretch(i, 2) + + info_group_box.setLayout(info_group_box_layout) + + layout.addWidget(info_group_box) + + attitude_group_box = QtW.QWidget() + attitude_group_box_layout = QtW.QGridLayout() + attitude_group_box_layout.addWidget(QtW.QLabel("ra"), 0, 0, 1, 1) + attitude_group_box_layout.addWidget(QtW.QLabel("dec"), 1, 0, 1, 1) + attitude_group_box_layout.addWidget(QtW.QLabel("roll"), 2, 0, 1, 1) + attitude_group_box_layout.addWidget(self.ra_edit, 0, 1, 1, 1) + attitude_group_box_layout.addWidget(self.dec_edit, 1, 1, 1, 1) + attitude_group_box_layout.addWidget(self.roll_edit, 2, 1, 1, 1) + attitude_group_box.setLayout(attitude_group_box_layout) + for i in range(3): + attitude_group_box_layout.setColumnStretch(i, 10) + + info_2_group_box = QtW.QWidget() + info_2_group_box_layout = QtW.QGridLayout() + info_2_group_box_layout.addWidget(QtW.QLabel("n_guide"), 0, 0, 1, 1) + info_2_group_box_layout.addWidget(self.n_guide_edit, 0, 1, 1, 1) + info_2_group_box_layout.addWidget(QtW.QLabel("n_fid"), 0, 2, 1, 1) + info_2_group_box_layout.addWidget(self.n_fid_edit, 0, 3, 1, 1) + info_2_group_box_layout.addWidget(QtW.QLabel("t_ccd"), 1, 0, 1, 1) + info_2_group_box_layout.addWidget(self.n_t_ccd_edit, 1, 1, 1, 1) + info_2_group_box_layout.addWidget(QtW.QLabel("Man. angle"), 1, 2, 1, 1) + info_2_group_box_layout.addWidget(self.man_angle_edit, 1, 3, 1, 1) + info_2_group_box.setLayout(info_2_group_box_layout) + for i in range(4): + info_2_group_box_layout.setColumnStretch(i, 1) + + dither_group_box = QtW.QWidget() + dither_group_box_layout = QtW.QGridLayout() + dither_group_box_layout.addWidget(QtW.QLabel(""), 0, 0, 1, 4) + dither_group_box_layout.addWidget(QtW.QLabel("y"), 0, 4, 1, 4) + dither_group_box_layout.addWidget(QtW.QLabel("z"), 0, 8, 1, 4) + dither_group_box_layout.addWidget(QtW.QLabel("acq"), 1, 0, 1, 4) + dither_group_box_layout.addWidget(self.dither_acq_y_edit, 1, 4, 1, 4) + dither_group_box_layout.addWidget(self.dither_acq_z_edit, 1, 8, 1, 4) + + dither_group_box_layout.addWidget(QtW.QLabel("guide"), 2, 0, 1, 4) + dither_group_box_layout.addWidget(self.dither_guide_y_edit, 2, 4, 1, 4) + dither_group_box_layout.addWidget(self.dither_guide_z_edit, 2, 8, 1, 4) + dither_group_box.setLayout(dither_group_box_layout) + + tab_2 = QtW.QTabWidget() + tab_2.addTab(attitude_group_box, "Attitude") + tab_2.addTab(dither_group_box, "Dither") + tab_2.addTab(info_2_group_box, "Other") + tab_2.setCurrentIndex(0) + layout.addWidget(tab_2) tab = QtW.QTabWidget() tab.addTab(self.include["acq"], "Include Acq.") @@ -96,10 +138,15 @@ def __init__(self, **kwargs): # noqa: PLR0915 tab.addTab(self.include["guide"], "Include Guide") tab.addTab(self.exclude["guide"], "Exclude Guide") tab.setCurrentIndex(0) - layout.addWidget(tab, 10, 0, 1, 2) + layout.addWidget(tab) + + controls_group_box = QtW.QGroupBox() + controls_group_box_layout = QtW.QVBoxLayout() + controls_group_box_layout.addWidget(self.do) + controls_group_box_layout.addWidget(self.run_sparkles_button) + controls_group_box.setLayout(controls_group_box_layout) - v_layout.addLayout(layout) - v_layout.addStretch(0) + layout.addWidget(controls_group_box) file = kwargs.pop("file", None) if file: @@ -137,6 +184,22 @@ def __init__(self, **kwargs): # noqa: PLR0915 kwargs["n_guide"] = "8" pprint(kwargs) + self.obsid_edit.setText(f"{kwargs.get('obsid', params.get('obsid', 0))}") + self.man_angle_edit.setText( + f"{kwargs.get('man_angle', params.get('man_angle', 0))}" + ) + self.dither_acq_y_edit.setText( + f"{kwargs.get('dither_acq_y', params.get('dither_acq_y', 16))}" + ) + self.dither_acq_z_edit.setText( + f"{kwargs.get('dither_acq_z', params.get('dither_acq_z', 16))}" + ) + self.dither_guide_y_edit.setText( + f"{kwargs.get('dither_guide_y', params.get('dither_guide_y', 16))}" + ) + self.dither_guide_z_edit.setText( + f"{kwargs.get('dither_guide_z', params.get('dither_guide_z', 16))}" + ) self.date_edit.setText(kwargs.get("date", params["date"])) self.ra_edit.setText(f"{kwargs.get('ra', params['attitude'].ra):.8f}") self.dec_edit.setText(f"{kwargs.get('dec', params['attitude'].dec):.8f}") @@ -148,7 +211,7 @@ def __init__(self, **kwargs): # noqa: PLR0915 kwargs.get("instrument", params["instrument"]) ) - self.setLayout(v_layout) + self.setLayout(layout) self.values = self._validate() self.do.clicked.connect(self._do_it) @@ -164,7 +227,7 @@ def _validate(self, quiet=False): try: n_fid = int(self.n_fid_edit.text()) n_guide = int(self.n_guide_edit.text()) - obsid = 0 if n_fid else 38000 + obsid = int(self.obsid_edit.text()) assert self.date_edit.text() != "", "No date" assert self.ra_edit.text() != "", "No RA" assert self.dec_edit.text() != "", "No dec" @@ -202,6 +265,15 @@ def _validate(self, quiet=False): int(self.include["guide"].item(i).text()) for i in range(self.include["guide"].count()) ], + "dither_acq": ( + int(self.dither_acq_y_edit.text()), + int(self.dither_acq_z_edit.text()), + ), + "dither_guide": ( + int(self.dither_guide_y_edit.text()), + int(self.dither_guide_z_edit.text()), + ), + "man_angle": float(self.man_angle_edit.text()), } except Exception as e: if not quiet: From ca70b6c7c37cf6541ea9c1e7daaa65e61eb24da7 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 18 Oct 2024 16:24:23 -0400 Subject: [PATCH 12/20] decrease the precision for ra/dec/roll --- aperoll/widgets/parameters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 940d9a5..5d3c5da 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -201,9 +201,9 @@ def __init__(self, **kwargs): # noqa: PLR0915 f"{kwargs.get('dither_guide_z', params.get('dither_guide_z', 16))}" ) self.date_edit.setText(kwargs.get("date", params["date"])) - self.ra_edit.setText(f"{kwargs.get('ra', params['attitude'].ra):.8f}") - self.dec_edit.setText(f"{kwargs.get('dec', params['attitude'].dec):.8f}") - self.roll_edit.setText(f"{kwargs.get('roll', params['attitude'].roll):.8f}") + self.ra_edit.setText(f"{kwargs.get('ra', params['attitude'].ra):.5f}") + self.dec_edit.setText(f"{kwargs.get('dec', params['attitude'].dec):.5f}") + self.roll_edit.setText(f"{kwargs.get('roll', params['attitude'].roll):.5f}") self.n_guide_edit.setText(f"{kwargs['n_guide']}") self.n_fid_edit.setText(f"{kwargs['n_fid']}") self.n_t_ccd_edit.setText(kwargs.get("t_ccd", f"{params['t_ccd']:.2f}")) From 493a3a413cd68fdd82c1a1c857726574d71b05a1 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 16:31:40 -0400 Subject: [PATCH 13/20] fix: clear item list when clearing the scene --- aperoll/widgets/star_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 8b8c166..4f42b4b 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -507,6 +507,7 @@ def show_test_stars_q(self, q, N=100, mag=10): def show_stars(self): self.scene.clear() + self._catalog_items = [] if self._base_attitude is None or self._time is None: return self.stars = get_stars(self._time, self._base_attitude) From 44f420abc3b6c89d7196824689538d0b3db524b5 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 18 Oct 2024 17:52:50 -0400 Subject: [PATCH 14/20] added option to read from proseco pickle, export proseco catalog to pickle, export to sparkles HTML, remove unused options, set dither to float, and moved option processing into separate functions (get_parameters_from_yoshi, get_parameters_from_pickle) --- aperoll/scripts/aperoll_main.py | 4 +- aperoll/widgets/main_window.py | 247 ++++++++++++++-------- aperoll/widgets/parameters.py | 358 ++++++++++++++++++++++++-------- 3 files changed, 432 insertions(+), 177 deletions(-) diff --git a/aperoll/scripts/aperoll_main.py b/aperoll/scripts/aperoll_main.py index b887596..d23afb5 100755 --- a/aperoll/scripts/aperoll_main.py +++ b/aperoll/scripts/aperoll_main.py @@ -11,9 +11,7 @@ def get_parser(): parse = argparse.ArgumentParser() parse.add_argument("file", nargs="?", default=None) - parse.add_argument("--date") - parse.add_argument("--ra") - parse.add_argument("--dec") + parse.add_argument("--obsid", type=int) return parse diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 414dba0..6bd8082 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -1,4 +1,7 @@ # from PyQt5 import QtCore as QtC, QtWidgets as QtW, QtGui as QtG +import os +import pickle +import tarfile from pathlib import Path from pprint import pprint from tempfile import TemporaryDirectory @@ -12,6 +15,7 @@ from proseco import get_aca_catalog from PyQt5 import QtCore as QtC from Quaternion import Quat +from ska_helpers import utils from .parameters import Parameters from .star_plot import StarPlot @@ -97,18 +101,28 @@ def on_url_changed(self, url): page.deleteLater() -class MainWindow(QtW.QWidget): +class MainWindow(QtW.QMainWindow): def __init__(self, opts=None): super().__init__() opts = {} if opts is None else opts opts = {k: opts[k] for k in opts if opts[k] is not None} pprint(opts) + self._main = QtW.QWidget() + self.setCentralWidget(self._main) - self.web_page = None + self.menu_bar = QtW.QMenuBar() + self.setMenuBar(self.menu_bar) - self._tmp_dir = TemporaryDirectory() - self._dir = Path(self._tmp_dir.name) + self.fileMenu = self.menu_bar.addMenu("&File") + export_action = QtW.QAction("&Export Pickle", self) + export_action.triggered.connect(self._export_proseco) + self.fileMenu.addAction(export_action) + export_action = QtW.QAction("&Export Sparkles", self) + export_action.triggered.connect(self._export_sparkles) + self.fileMenu.addAction(export_action) + + self.web_page = None self.plot = StarPlot() self.parameters = Parameters(**opts) @@ -117,7 +131,7 @@ def __init__(self, opts=None): font.setPixelSize(5) # setting a pixel size so it can be changed later self.textEdit.setFont(font) - layout = QtW.QVBoxLayout(self) + layout = QtW.QVBoxLayout(self._main) layout_2 = QtW.QHBoxLayout() layout.addWidget(self.parameters) @@ -127,7 +141,7 @@ def __init__(self, opts=None): layout.setStretch(0, 1) # the dialog on top should not stretch much layout.setStretch(1, 10) - self.setLayout(layout) + self._main.setLayout(layout) self.plot.include_star.connect(self.parameters.include_star) # self.plot.exclude_star.connect(self.parameters.exclude_star) @@ -135,8 +149,12 @@ def __init__(self, opts=None): self.parameters.do_it.connect(self._run_proseco) self.parameters.run_sparkles.connect(self._run_sparkles) self.parameters.draw_test.connect(self._draw_test) + self.parameters.parameters_changed.connect(self._parameters_changed) self.plot.attitude_changed.connect(self.parameters.set_ra_dec) + self._data = Data(self.parameters.proseco_args()) + self.outdir = Path(os.getcwd()) + self._init() def closeEvent(self, event): @@ -145,22 +163,17 @@ def closeEvent(self, event): self.web_page = None event.accept() + def _parameters_changed(self): + self._data.reset(self.parameters.proseco_args()) + def _init(self): if self.parameters.values: - # obsid = self.parameters.values["obsid"] ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] roll = self.parameters.values["roll"] time = CxoTime(self.parameters.values["date"]) - - # aca_attitude = calc_aca_from_targ( - # Quat(equatorial=(float(ra / u.deg), float(dec / u.deg), nominal_roll)), - # 0, - # 0 - # ) aca_attitude = Quat( equatorial=(float(ra / u.deg), float(dec / u.deg), roll) ) - # print("ra, dec, roll =", (float(ra / u.deg), float(dec / u.deg), roll)) self.plot.set_base_attitude(aca_attitude, update=False) self.plot.set_time(time, update=True) @@ -171,63 +184,74 @@ def _draw_test(self): aca_attitude = Quat( equatorial=(float(ra / u.deg), float(dec / u.deg), roll) ) - # self.plot.show_test_stars_q(aca_attitude) dq = self.plot._base_attitude.dq(aca_attitude) self.plot.show_test_stars( ra_offset=dq.ra, dec_offset=dq.dec, roll_offset=dq.roll ) - def _proseco_args(self): - obsid = self.parameters.values["obsid"] - ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] - roll = self.parameters.values["roll"] - time = CxoTime(self.parameters.values["date"]) - - aca_attitude = Quat(equatorial=(float(ra / u.deg), float(dec / u.deg), roll)) - - args = { - "obsid": obsid, - "att": aca_attitude, - "date": time, - "n_fid": self.parameters.values["n_fid"], - "n_guide": self.parameters.values["n_guide"], - "dither_acq": self.parameters.values["dither_acq"], - "dither_guide": self.parameters.values["dither_guide"], - "t_ccd_acq": self.parameters.values["t_ccd"], - "t_ccd_guide": self.parameters.values["t_ccd"], - "man_angle": self.parameters.values["man_angle"], - "detector": self.parameters.values["instrument"], - "sim_offset": 0, # docs say this is optional, but it does not seem to be - "focus_offset": 0, # docs say this is optional, but it does not seem to be - } - - for key in [ - "exclude_ids_guide", - "include_ids_guide", - "exclude_ids_acq", - "include_ids_acq", - ]: - if self.parameters.values[key]: - args[key] = self.parameters.values[key] - - return args - def _run_proseco(self): - print("parameters:", self.parameters.values) - if self.parameters.values: - args = self._proseco_args() - pprint(args) - catalog = get_aca_catalog(**args) - self.plot.set_catalog(catalog, update=False) - - aca = catalog.get_review_table() - - sparkles.core.check_catalog(aca) - - # aca.messages + """ + Display the star catalog. + """ + if self._data.proseco: + self.textEdit.setText( + f"{STYLE}
{self._data.proseco['aca'].get_text_pre()}
" + ) + self.plot.set_catalog(self._data.proseco["catalog"], update=False) + + def _export_proseco(self): + """ + Save the star catalog in a pickle file. + """ + if self._data.proseco: + catalog = self._data.proseco["catalog"] + dialog = QtW.QFileDialog( + self, + "Export Pickle", + str(self.outdir / f"aperoll-obsid_{catalog.obsid:.0f}.pkl"), + ) + dialog.setAcceptMode(QtW.QFileDialog.AcceptSave) + dialog.setDefaultSuffix("pkl") + rc = dialog.exec() + if rc: + self._data.export_proseco(dialog.selectedFiles()[0]) + + def _export_sparkles(self): + """ + Save the sparkles report to a tarball. + """ + if self._data.sparkles: + catalog = self._data.proseco["catalog"] + # for some reason, the extension hidden but it works + dialog = QtW.QFileDialog( + self, + "Export Pickle", + str(self.outdir / f"aperoll-obsid_{catalog.obsid:.0f}.tgz"), + ) + dialog.setAcceptMode(QtW.QFileDialog.AcceptSave) + dialog.setDefaultSuffix(".tgz") + rc = dialog.exec() + if rc: + self._data.export_sparkles(dialog.selectedFiles()[0]) - # self.textEdit.setText(table_to_html(catalog)) - self.textEdit.setText(f"{STYLE}
{aca.get_text_pre()}
") + def _run_sparkles(self): + """ + Display the sparkles report in a web browser. + """ + if self._data.sparkles: + try: + w = QtW.QMainWindow(self) + w.resize(1400, 1000) + web = QtWe.QWebEngineView(w) + w.setCentralWidget(web) + self.web_page = WebPage() + web.setPage(self.web_page) + url = self._data.sparkles / "index.html" + web.load(QtC.QUrl(f"file://{url}")) + web.show() + w.show() + except Exception as e: + print(e) def resizeEvent(self, _size): font = self.textEdit.font() @@ -247,33 +271,82 @@ def resizeEvent(self, _size): font.setPixelSize(pix_size) self.textEdit.setFont(font) - def _run_sparkles(self): - # print("parameters:", self.parameters.values) - if self.parameters.values: - args = self._proseco_args() - pprint(args) - catalog = get_aca_catalog(**args) +class CachedVal: + def __init__(self, func): + self._func = func + self.reset() + + def reset(self): + self._value = utils.LazyVal(self._func) + + @property + def val(self): + return self._value.val + + +class Data: + def __init__(self, parameters=None) -> None: + self._proseco = CachedVal(self.run_proseco) + self._sparkles = CachedVal(self.run_sparkles) + self.parameters = parameters + self._tmp_dir = TemporaryDirectory() + self._dir = Path(self._tmp_dir.name) + + def reset(self, parameters): + self.parameters = parameters + self._proseco.reset() + self._sparkles.reset() + + @property + def proseco(self): + return self._proseco.val + + @property + def sparkles(self): + return self._sparkles.val + + def export_proseco(self, outfile): + if self.proseco: + outfile = self._dir / outfile + catalog = self.proseco["catalog"] + if catalog: + with open(outfile, "wb") as fh: + pickle.dump({catalog.obsid: catalog}, fh) + + def export_sparkles(self, outfile): + if self.sparkles: + outfile = self._dir / outfile + if self.sparkles: + dest = Path(outfile.name.replace(".tar", "").replace(".gz", "")) + with tarfile.open(outfile, "w") as tar: + for name in self.sparkles.glob("**/*"): + tar.add( + name, + arcname=dest / name.relative_to(self._dir / "sparkles"), + ) + + def run_proseco(self): + # print("parameters:", self.parameters) + if self.parameters: + pprint(self.parameters) + catalog = get_aca_catalog(**self.parameters) + aca = catalog.get_review_table() + sparkles.core.check_catalog(aca) + + return { + "catalog": catalog, + "aca": aca, + } + return {} + + def run_sparkles(self): + if self.proseco and self.proseco["catalog"]: sparkles.run_aca_review( "Exploration", - acars=[catalog.get_review_table()], + acars=[self.proseco["catalog"].get_review_table()], report_dir=self._dir / "sparkles", report_level="all", roll_level="none", ) - print(f"sparkles report at {self._dir / 'sparkles'}") - try: - w = QtW.QMainWindow(self) - w.resize(1400, 1000) - web = QtWe.QWebEngineView(w) - w.setCentralWidget(web) - self.web_page = WebPage() - web.setPage(self.web_page) - url = self._dir / "sparkles" / "index.html" - web.load(QtC.QUrl(f"file://{url}")) - web.show() - w.show() - except Exception as e: - print(e) - - self.plot.set_catalog(catalog, update=False) + return self._dir / "sparkles" diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 5d3c5da..a6a046a 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -1,22 +1,58 @@ +import gzip import json +import pickle from pprint import pprint +import maude import Ska.Sun as sun from astropy import units as u from cxotime.cxotime import CxoTime +from kadi.commands.observations import get_detector_and_sim_offset from PyQt5 import QtCore as QtC from PyQt5 import QtWidgets as QtW +from Quaternion import Quat -def get_parameters(): - import maude - from Quaternion import Quat +class LineEdit(QtW.QLineEdit): + """ + A QLineEdit with a signal emitted when pressing Enter, loosing focus or calling setText. + + The signal is emitted only if the text changed. + """ + value_changed = QtC.pyqtSignal(str) + + def __init__(self, parent): + super().__init__(parent) + self._prev_text = self.text() + + def setText(self, text): + super().setText(text) + self._check_value() + + def keyPressEvent(self, event): + super().keyPressEvent(event) + if event.key() == QtC.Qt.Key_Return: + self._check_value() + + def focusOutEvent(self, event): + super().focusOutEvent(event) + self._check_value() + + def _check_value(self): + if self.text() != self._prev_text: + self._prev_text = self.text() + self.value_changed.emit(self.text()) + + +def get_default_parameters(): + """ + Get default initial parameters from current telemetry. + """ msid_list = ["3TSCPOS", "AACCCDPT"] + [f"aoattqt{i}".upper() for i in range(1, 5)] msids = maude.get_msids(msid_list) data = {msid: msids["data"][i]["values"][-1] for i, msid in enumerate(msid_list)} q = Quat(q=[data[f"AOATTQT{i}"] for i in range(1, 5)]) - from kadi.commands.observations import get_detector_and_sim_offset instrument, sim_offset = get_detector_and_sim_offset(data["3TSCPOS"]) t_ccd = (data["AACCCDPT"] - 32) * 5 / 9 @@ -24,35 +60,147 @@ def get_parameters(): result = { "date": CxoTime().date, "attitude": q, + "ra": q.ra, + "dec": q.dec, + "roll": q.roll, "instrument": instrument, "sim_offset": sim_offset, "t_ccd": t_ccd, + "obsid": 0, + "man_angle": 0, + "dither_acq_y": 16, + "dither_acq_z": 16, + "dither_guide_y": 16, + "dither_guide_z": 16, + "n_fid": 0, + "n_guide": 8, } - from pprint import pprint - pprint(result) return result +def get_parameters_from_yoshi(filename, obsid=None): + """ + Get initial parameters from a Yoshi JSON file. + """ + + with open(filename) as fh: + contents = json.load(fh) + if obsid is not None: + contents = [obs for obs in contents if obs["obsid"] == obsid] + if not contents: + raise Exception(f"OBSID {obsid} not found in {filename}") + + if not contents: + raise Exception(f"No entries found in {filename}") + + yoshi_params = contents[0] # assuming there is only one entry + + yoshi_params["date"] = yoshi_params["obs_date"] + yoshi_params["ra"] = yoshi_params["ra_targ"] + yoshi_params["dec"] = yoshi_params["dec_targ"] + yoshi_params["roll"] = yoshi_params["roll_targ"] + yoshi_params["instrument"] = yoshi_params["detector"] + for key in [ + "obs_date", + "ra_targ", + "dec_targ", + "roll_targ", + "detector", + ]: + del yoshi_params[key] + + if abs(yoshi_params.get("obsid", 0)) < 38000: + yoshi_params["n_fid"] = "3" + yoshi_params["n_guide"] = "5" + else: + yoshi_params["n_fid"] = "0" + yoshi_params["n_guide"] = "8" + + att = Quat( + equatorial=(yoshi_params["ra"], yoshi_params["dec"], yoshi_params["roll"]) + ) + + default = get_default_parameters() + parameters = { + "obsid": yoshi_params.get("obsid", default.get("obsid", 0)), + "man_angle": yoshi_params.get("man_angle", default.get("man_angle", 0)), + "dither_acq_y": yoshi_params.get("dither_y", default.get("dither_acq_y", 16)), + "dither_acq_z": yoshi_params.get("dither_z", default.get("dither_acq_z", 16)), + "dither_guide_y": yoshi_params.get( + "dither_y", default.get("dither_guide_y", 16) + ), + "dither_guide_z": yoshi_params.get( + "dither_z", default.get("dither_guide_z", 16) + ), + "date": yoshi_params.get("date", default["date"]), + "attitude": att, + "ra": yoshi_params.get("ra", default["attitude"].ra), + "dec": yoshi_params.get("dec", default["attitude"].dec), + "roll": yoshi_params.get("roll", default["attitude"].roll), + "t_ccd": yoshi_params.get("t_ccd", default["t_ccd"]), + "instrument": yoshi_params.get("instrument", default["instrument"]), + "n_guide": yoshi_params["n_guide"], + "n_fid": yoshi_params["n_fid"], + } + return parameters + + +def get_parameters_from_pickle(filename, obsid=None): + """ + Get initial parameters from a proseco pickle file. + """ + open_fcn = open if filename.endswith(".pkl") else gzip.open + with open_fcn(filename, "rb") as fh: + catalogs = pickle.load(fh) + + if float(obsid) not in catalogs: + raise Exception(f"OBSID {obsid} not found in {filename}") + + catalog = catalogs[float(obsid)] + + parameters = { + "obsid": obsid, + "man_angle": catalog.man_angle, + "dither_acq_y": catalog.dither_acq.y, + "dither_acq_z": catalog.dither_acq.z, + "dither_guide_y": catalog.dither_guide.y, + "dither_guide_z": catalog.dither_guide.z, + "date": CxoTime(catalog.date).date, # date is not guaranteed to be a fixed type in pickle + "attitude": catalog.att, + "ra": catalog.att.ra, + "dec": catalog.att.dec, + "roll": catalog.att.roll, + "t_ccd": catalog.t_ccd, + "instrument": catalog.detector, + "n_guide": catalog.n_guide, + "n_fid": catalog.n_fid, + } + return parameters + + class Parameters(QtW.QWidget): do_it = QtC.pyqtSignal() run_sparkles = QtC.pyqtSignal() draw_test = QtC.pyqtSignal() + reset = QtC.pyqtSignal() + parameters_changed = QtC.pyqtSignal() def __init__(self, **kwargs): # noqa: PLR0915 super().__init__() - self.date_edit = QtW.QLineEdit(self) - self.obsid_edit = QtW.QLineEdit(self) - self.ra_edit = QtW.QLineEdit(self) - self.dec_edit = QtW.QLineEdit(self) - self.roll_edit = QtW.QLineEdit(self) - self.n_guide_edit = QtW.QLineEdit(self) - self.n_fid_edit = QtW.QLineEdit(self) - self.n_t_ccd_edit = QtW.QLineEdit(self) - self.man_angle_edit = QtW.QLineEdit(self) + self.date_edit = LineEdit(self) + self.obsid_edit = LineEdit(self) + self.ra_edit = LineEdit(self) + self.dec_edit = LineEdit(self) + self.roll_edit = LineEdit(self) + self.n_guide_edit = LineEdit(self) + self.n_fid_edit = LineEdit(self) + self.n_t_ccd_edit = LineEdit(self) + self.man_angle_edit = LineEdit(self) self.instrument_edit = QtW.QComboBox(self) self.instrument_edit.addItems(["ACIS-S", "ACIS-I", "HRC-S", "HRC-I"]) self.do = QtW.QPushButton("Get Catalog") + self.reset_button = QtW.QPushButton("Reset") self.run_sparkles_button = QtW.QPushButton("Run Sparkles") self.draw_test_button = QtW.QPushButton("Draw Test") self.include = { @@ -63,10 +211,33 @@ def __init__(self, **kwargs): # noqa: PLR0915 "acq": QtW.QListWidget(self), "guide": QtW.QListWidget(self), } - self.dither_acq_y_edit = QtW.QLineEdit(self) - self.dither_acq_z_edit = QtW.QLineEdit(self) - self.dither_guide_y_edit = QtW.QLineEdit(self) - self.dither_guide_z_edit = QtW.QLineEdit(self) + self.dither_acq_y_edit = LineEdit(self) + self.dither_acq_z_edit = LineEdit(self) + self.dither_guide_y_edit = LineEdit(self) + self.dither_guide_z_edit = LineEdit(self) + + self.date_edit.value_changed.connect(self._values_changed) + self.obsid_edit.value_changed.connect(self._values_changed) + self.ra_edit.value_changed.connect(self._values_changed) + self.dec_edit.value_changed.connect(self._values_changed) + self.roll_edit.value_changed.connect(self._values_changed) + self.n_guide_edit.value_changed.connect(self._values_changed) + self.n_fid_edit.value_changed.connect(self._values_changed) + self.n_t_ccd_edit.value_changed.connect(self._values_changed) + self.man_angle_edit.value_changed.connect(self._values_changed) + self.instrument_edit.currentIndexChanged.connect(self._values_changed) + self.include["acq"].model().rowsInserted.connect(self._values_changed) + self.include["guide"].model().rowsInserted.connect(self._values_changed) + self.exclude["acq"].model().rowsInserted.connect(self._values_changed) + self.exclude["guide"].model().rowsInserted.connect(self._values_changed) + self.include["acq"].model().rowsRemoved.connect(self._values_changed) + self.include["guide"].model().rowsRemoved.connect(self._values_changed) + self.exclude["acq"].model().rowsRemoved.connect(self._values_changed) + self.exclude["guide"].model().rowsRemoved.connect(self._values_changed) + self.dither_acq_y_edit.value_changed.connect(self._values_changed) + self.dither_acq_z_edit.value_changed.connect(self._values_changed) + self.dither_guide_y_edit.value_changed.connect(self._values_changed) + self.dither_guide_z_edit.value_changed.connect(self._values_changed) layout = QtW.QHBoxLayout() @@ -144,85 +315,63 @@ def __init__(self, **kwargs): # noqa: PLR0915 controls_group_box_layout = QtW.QVBoxLayout() controls_group_box_layout.addWidget(self.do) controls_group_box_layout.addWidget(self.run_sparkles_button) + controls_group_box_layout.addWidget(self.reset_button) controls_group_box.setLayout(controls_group_box_layout) layout.addWidget(controls_group_box) - file = kwargs.pop("file", None) - if file: - with open(file) as fh: - file_kwargs = json.load(fh)[0] # assuming there is only one entry - - file_kwargs["date"] = file_kwargs["obs_date"] - file_kwargs["ra"] = file_kwargs["ra_targ"] - file_kwargs["dec"] = file_kwargs["dec_targ"] - file_kwargs["roll"] = file_kwargs["roll_targ"] - file_kwargs["dither"] = ( - file_kwargs["dither_y"], - file_kwargs["dither_z"], - ) - file_kwargs["instrument"] = file_kwargs["detector"] - for key in [ - "obs_date", - "ra_targ", - "dec_targ", - "roll_targ", - "dither_y", - "dither_z", - "detector", - ]: - del file_kwargs[key] - kwargs.update(file_kwargs) - - params = get_parameters() - - if abs(kwargs.get("obsid", 0)) < 38000: - kwargs["n_fid"] = "3" - kwargs["n_guide"] = "5" - else: - kwargs["n_fid"] = "0" - kwargs["n_guide"] = "8" - - pprint(kwargs) - self.obsid_edit.setText(f"{kwargs.get('obsid', params.get('obsid', 0))}") - self.man_angle_edit.setText( - f"{kwargs.get('man_angle', params.get('man_angle', 0))}" - ) - self.dither_acq_y_edit.setText( - f"{kwargs.get('dither_acq_y', params.get('dither_acq_y', 16))}" - ) - self.dither_acq_z_edit.setText( - f"{kwargs.get('dither_acq_z', params.get('dither_acq_z', 16))}" - ) - self.dither_guide_y_edit.setText( - f"{kwargs.get('dither_guide_y', params.get('dither_guide_y', 16))}" - ) - self.dither_guide_z_edit.setText( - f"{kwargs.get('dither_guide_z', params.get('dither_guide_z', 16))}" - ) - self.date_edit.setText(kwargs.get("date", params["date"])) - self.ra_edit.setText(f"{kwargs.get('ra', params['attitude'].ra):.5f}") - self.dec_edit.setText(f"{kwargs.get('dec', params['attitude'].dec):.5f}") - self.roll_edit.setText(f"{kwargs.get('roll', params['attitude'].roll):.5f}") - self.n_guide_edit.setText(f"{kwargs['n_guide']}") - self.n_fid_edit.setText(f"{kwargs['n_fid']}") - self.n_t_ccd_edit.setText(kwargs.get("t_ccd", f"{params['t_ccd']:.2f}")) - self.instrument_edit.setCurrentText( - kwargs.get("instrument", params["instrument"]) - ) - self.setLayout(layout) - self.values = self._validate() self.do.clicked.connect(self._do_it) self.draw_test_button.clicked.connect(self._draw_test) self.run_sparkles_button.clicked.connect(self.run_sparkles) + self.reset_button.clicked.connect(self.reset) + + self.set_parameters(**kwargs) + + def set_parameters(self, **kwargs): + if "file" in kwargs and ( + kwargs["file"].endswith(".pkl") or kwargs["file"].endswith(".pkl.gz") + ): + params = get_parameters_from_pickle( + kwargs["file"], obsid=kwargs.get("obsid", None) + ) + elif "file" in kwargs and kwargs["file"].endswith(".json"): + params = get_parameters_from_yoshi( + kwargs["file"], obsid=kwargs.get("obsid", None) + ) + else: + params = get_default_parameters() + + pprint(params) + self.obsid_edit.setText(f"{params['obsid']}") + self.man_angle_edit.setText(f"{params['man_angle']}") + self.dither_acq_y_edit.setText(f"{params['dither_acq_y']}") + self.dither_acq_z_edit.setText(f"{params['dither_acq_z']}") + self.dither_guide_y_edit.setText(f"{params['dither_guide_y']}") + self.dither_guide_z_edit.setText(f"{params['dither_guide_z']}") + self.date_edit.setText(kwargs.get("date", params["date"])) + self.ra_edit.setText(f"{params['ra']:.5f}") + self.dec_edit.setText(f"{params['dec']:.5f}") + self.roll_edit.setText(f"{params['roll']:.5f}") + self.n_guide_edit.setText(f"{params['n_guide']}") + self.n_fid_edit.setText(f"{params['n_fid']}") + self.n_t_ccd_edit.setText(f"{params['t_ccd']:.2f}") + self.instrument_edit.setCurrentText(params["instrument"]) + + self.values = self._validate() def _draw_test(self): self.values = self._validate() if self.values: self.draw_test.emit() + def _values_changed(self): + # values are empty if validation fails, but the signal is still emitted to notify anyone + # that the values have changed + self.values = self._validate(quiet=True) + self.parameters_changed.emit() + def _validate(self, quiet=False): try: n_fid = int(self.n_fid_edit.text()) @@ -266,12 +415,12 @@ def _validate(self, quiet=False): for i in range(self.include["guide"].count()) ], "dither_acq": ( - int(self.dither_acq_y_edit.text()), - int(self.dither_acq_z_edit.text()), + float(self.dither_acq_y_edit.text()), + float(self.dither_acq_z_edit.text()), ), "dither_guide": ( - int(self.dither_guide_y_edit.text()), - int(self.dither_guide_z_edit.text()), + float(self.dither_guide_y_edit.text()), + float(self.dither_guide_z_edit.text()), ), "man_angle": float(self.man_angle_edit.text()), } @@ -318,3 +467,38 @@ def _exclude_star(self, star, type, exclude): else: for it in items: self.exclude[type].takeItem(self.exclude[type].row(it)) + + def proseco_args(self): + obsid = self.values["obsid"] + ra, dec = self.values["ra"], self.values["dec"] + roll = self.values["roll"] + time = CxoTime(self.values["date"]) + + aca_attitude = Quat(equatorial=(float(ra / u.deg), float(dec / u.deg), roll)) + + args = { + "obsid": obsid, + "att": aca_attitude, + "date": time, + "n_fid": self.values["n_fid"], + "n_guide": self.values["n_guide"], + "dither_acq": self.values["dither_acq"], + "dither_guide": self.values["dither_guide"], + "t_ccd_acq": self.values["t_ccd"], + "t_ccd_guide": self.values["t_ccd"], + "man_angle": self.values["man_angle"], + "detector": self.values["instrument"], + "sim_offset": 0, # docs say this is optional, but it does not seem to be + "focus_offset": 0, # docs say this is optional, but it does not seem to be + } + + for key in [ + "exclude_ids_guide", + "include_ids_guide", + "exclude_ids_acq", + "include_ids_acq", + ]: + if self.values[key]: + args[key] = self.values[key] + + return args From 824581fc3dbc07837645b5d74e4e6f89236b8a7a Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 17:04:40 -0400 Subject: [PATCH 15/20] Add StarcatView widget --- aperoll/widgets/main_window.py | 31 ++------ aperoll/widgets/starcat_view.py | 121 ++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 aperoll/widgets/starcat_view.py diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 6bd8082..b949f7f 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -6,7 +6,6 @@ from pprint import pprint from tempfile import TemporaryDirectory -import PyQt5.QtGui as QtG import PyQt5.QtWebEngineWidgets as QtWe import PyQt5.QtWidgets as QtW import sparkles @@ -19,6 +18,7 @@ from .parameters import Parameters from .star_plot import StarPlot +from .starcat_view import StarcatView STYLE = """ +""" + + +class WebPage(QtWe.QWebEnginePage): + def __init__(self, parent=None): + super().__init__(parent) + + # trick from https://stackoverflow.com/questions/54920726/how-make-any-link-blank-open-in-same-window-using-qwebengine + def createWindow(self, _type): + page = WebPage(self) + page.urlChanged.connect(self.on_url_changed) + return page + + @QtC.pyqtSlot(QtC.QUrl) + def on_url_changed(self, url): + page = self.sender() + self.setUrl(url) + page.deleteLater() + + +class StarcatView(QtW.QTextEdit): + def __init__(self, catalog=None, parent=None): + super().__init__(parent) + font = QtG.QFont("Courier New") # setting a fixed-width font (close enough) + font.setPixelSize(12) # setting a pixel size so it can be changed later + self.setFont(font) + + self.set_catalog(catalog) + + def reset(self): + self.set_catalog(None) + + def set_catalog(self, catalog): + if catalog is None: + self.setText("") + else: + self.setText(f"{STYLE}
{catalog.get_text_pre()}
") + + def resizeEvent(self, _size): + super().resizeEvent(_size) + font = self.font() + header = ( + "idx slot id type sz p_acq mag mag_err " + "maxmag yang zang row col dim res halfw" + ) + n_lines = 35 + scale_x = float(0.9 * self.width()) / QtG.QFontMetrics(font).width(header) + scale_y = float(0.9 * self.height()) / ( + n_lines * QtG.QFontMetrics(font).height() + ) + pix_size = int(font.pixelSize() * min(scale_x, scale_y)) + if pix_size > 0: + font.setPixelSize(pix_size) + self.setFont(font) From 6bd7a9e4670b979cd9fd0bc9ededcfeadf308dbe Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 17:05:00 -0400 Subject: [PATCH 16/20] enable reset feature --- aperoll/widgets/main_window.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index b949f7f..364814e 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -108,6 +108,9 @@ def __init__(self, opts=None): opts = {k: opts[k] for k in opts if opts[k] is not None} pprint(opts) + + self.opts = opts + self._main = QtW.QWidget() self.setCentralWidget(self._main) @@ -145,6 +148,7 @@ def __init__(self, opts=None): self.parameters.do_it.connect(self._run_proseco) self.parameters.run_sparkles.connect(self._run_sparkles) + self.parameters.reset.connect(self._reset) self.parameters.draw_test.connect(self._draw_test) self.parameters.parameters_changed.connect(self._parameters_changed) self.plot.attitude_changed.connect(self.parameters.set_ra_dec) @@ -174,6 +178,13 @@ def _init(self): self.plot.set_base_attitude(aca_attitude, update=False) self.plot.set_time(time, update=True) + def _reset(self): + self.parameters.set_parameters(**self.opts) + self.starcat_view.reset() + self._data.reset(self.parameters.proseco_args()) + self._init() + + def _draw_test(self): if self.parameters.values: ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] From 051bdcbc8879e9989c2a1ac331505f7f530afd8a Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 18:24:31 -0400 Subject: [PATCH 17/20] show catalog from proseco pickle --- aperoll/widgets/main_window.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 364814e..0049b34 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -158,6 +158,29 @@ def __init__(self, opts=None): self._init() + starcat = None + if "file" in opts: + filename = opts.get("file") + catalogs = {} + if filename.endswith(".pkl"): + with open(filename, "rb") as fh: + catalogs = pickle.load(fh) + elif filename.endswith(".pkl.gz"): + with gzip.open(filename, "rb") as fh: + catalogs = pickle.load(fh) + if catalogs: + obsids = [int(np.round(float(k))) for k in catalogs] + if "obsid" not in opts or opts["obsid"] is None: + starcat = catalogs[obsids[0]] + else: + starcat = catalogs[opts["obsid"]] + aca = starcat.get_review_table() + sparkles.core.check_catalog(aca) + + if starcat is not None: + self.plot.set_catalog(starcat, update=False) + self.starcat_view.set_catalog(aca) + def closeEvent(self, event): if self.web_page is not None: del self.web_page From 7f6ad9ab7e2cba4eb5c093b1b2e211594fc82759 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 18:21:01 -0400 Subject: [PATCH 18/20] add logger and AperollException --- aperoll/scripts/aperoll_main.py | 30 +++++++++++++++++++++++------- aperoll/utils.py | 7 +++++++ aperoll/widgets/main_window.py | 7 +++---- aperoll/widgets/parameters.py | 17 +++++++++++------ 4 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 aperoll/utils.py diff --git a/aperoll/scripts/aperoll_main.py b/aperoll/scripts/aperoll_main.py index d23afb5..101c401 100755 --- a/aperoll/scripts/aperoll_main.py +++ b/aperoll/scripts/aperoll_main.py @@ -3,6 +3,7 @@ # from PyQt5 import QtCore as QtC, QtWidgets as QtW, QtGui as QtG from PyQt5 import QtWidgets as QtW +from aperoll.utils import AperollException, logger from aperoll.widgets.main_window import MainWindow @@ -12,17 +13,32 @@ def get_parser(): parse = argparse.ArgumentParser() parse.add_argument("file", nargs="?", default=None) parse.add_argument("--obsid", type=int) + levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + levels = [lvl.lower() for lvl in levels] + parse.add_argument( + "--log-level", help="Set the log level", default="INFO", choices=levels + ) return parse def main(): - args = get_parser().parse_args() - - app = QtW.QApplication([]) - w = MainWindow(opts=vars(args)) - w.resize(1500, 1000) - w.show() - app.exec() + parser = get_parser() + args = parser.parse_args() + + logger.setLevel(args.log_level) + + try: + app = QtW.QApplication([]) + w = MainWindow(opts=vars(args)) + w.resize(1500, 1000) + w.show() + app.exec() + except AperollException as e: + logger.error(f"Error: {e}") + parser.exit(1) + except FileNotFoundError as e: + logger.error(f"Error: {e}") + parser.exit(1) if __name__ == "__main__": diff --git a/aperoll/utils.py b/aperoll/utils.py new file mode 100644 index 0000000..925dfd5 --- /dev/null +++ b/aperoll/utils.py @@ -0,0 +1,7 @@ +from ska_helpers import logging + +logger = logging.basic_logger("aperoll") + + +class AperollException(RuntimeError): + pass diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index 0049b34..bdfe8da 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -3,7 +3,6 @@ import pickle import tarfile from pathlib import Path -from pprint import pprint from tempfile import TemporaryDirectory import PyQt5.QtWebEngineWidgets as QtWe @@ -16,6 +15,8 @@ from Quaternion import Quat from ska_helpers import utils +from aperoll.utils import logger + from .parameters import Parameters from .star_plot import StarPlot from .starcat_view import StarcatView @@ -280,7 +281,7 @@ def _run_sparkles(self): web.show() w.show() except Exception as e: - print(e) + logger.warning(e) class CachedVal: @@ -338,9 +339,7 @@ def export_sparkles(self, outfile): ) def run_proseco(self): - # print("parameters:", self.parameters) if self.parameters: - pprint(self.parameters) catalog = get_aca_catalog(**self.parameters) aca = catalog.get_review_table() sparkles.core.check_catalog(aca) diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index a6a046a..2190b23 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -1,7 +1,7 @@ import gzip import json import pickle -from pprint import pprint +from pprint import pformat import maude import Ska.Sun as sun @@ -12,6 +12,8 @@ from PyQt5 import QtWidgets as QtW from Quaternion import Quat +from aperoll.utils import AperollException, logger + class LineEdit(QtW.QLineEdit): """ @@ -89,10 +91,10 @@ def get_parameters_from_yoshi(filename, obsid=None): if obsid is not None: contents = [obs for obs in contents if obs["obsid"] == obsid] if not contents: - raise Exception(f"OBSID {obsid} not found in {filename}") + raise AperollException(f"OBSID {obsid} not found in {filename}") if not contents: - raise Exception(f"No entries found in {filename}") + raise AperollException(f"No entries found in {filename}") yoshi_params = contents[0] # assuming there is only one entry @@ -154,8 +156,11 @@ def get_parameters_from_pickle(filename, obsid=None): with open_fcn(filename, "rb") as fh: catalogs = pickle.load(fh) + if not catalogs: + raise AperollException(f"No entries found in {filename}") + if float(obsid) not in catalogs: - raise Exception(f"OBSID {obsid} not found in {filename}") + raise AperollException(f"OBSID {obsid} not found in {filename}") catalog = catalogs[float(obsid)] @@ -343,7 +348,7 @@ def set_parameters(self, **kwargs): else: params = get_default_parameters() - pprint(params) + logger.debug(pformat(params)) self.obsid_edit.setText(f"{params['obsid']}") self.man_angle_edit.setText(f"{params['man_angle']}") self.dither_acq_y_edit.setText(f"{params['dither_acq_y']}") @@ -426,7 +431,7 @@ def _validate(self, quiet=False): } except Exception as e: if not quiet: - print(e) + logger.warning(e) return {} def _do_it(self): From a64d46f60bb6808d29c54d248ed7ec96cbb82a4e Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 18:32:28 -0400 Subject: [PATCH 19/20] small changes (handle default/missing OBSID and little things) --- aperoll/scripts/aperoll_main.py | 2 +- aperoll/widgets/main_window.py | 7 +++---- aperoll/widgets/parameters.py | 9 +++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/aperoll/scripts/aperoll_main.py b/aperoll/scripts/aperoll_main.py index 101c401..0adf94c 100755 --- a/aperoll/scripts/aperoll_main.py +++ b/aperoll/scripts/aperoll_main.py @@ -12,7 +12,7 @@ def get_parser(): parse = argparse.ArgumentParser() parse.add_argument("file", nargs="?", default=None) - parse.add_argument("--obsid", type=int) + parse.add_argument("--obsid", help="Specify the OBSID", type=int) levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] levels = [lvl.lower() for lvl in levels] parse.add_argument( diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index bdfe8da..8ab6232 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -1,10 +1,12 @@ # from PyQt5 import QtCore as QtC, QtWidgets as QtW, QtGui as QtG +import gzip import os import pickle import tarfile from pathlib import Path from tempfile import TemporaryDirectory +import numpy as np import PyQt5.QtWebEngineWidgets as QtWe import PyQt5.QtWidgets as QtW import sparkles @@ -103,13 +105,11 @@ def on_url_changed(self, url): class MainWindow(QtW.QMainWindow): - def __init__(self, opts=None): + def __init__(self, opts=None): # noqa: PLR0915 super().__init__() opts = {} if opts is None else opts opts = {k: opts[k] for k in opts if opts[k] is not None} - pprint(opts) - self.opts = opts self._main = QtW.QWidget() @@ -208,7 +208,6 @@ def _reset(self): self._data.reset(self.parameters.proseco_args()) self._init() - def _draw_test(self): if self.parameters.values: ra, dec = self.parameters.values["ra"], self.parameters.values["dec"] diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index 2190b23..f4a1539 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -4,6 +4,7 @@ from pprint import pformat import maude +import numpy as np import Ska.Sun as sun from astropy import units as u from cxotime.cxotime import CxoTime @@ -21,6 +22,7 @@ class LineEdit(QtW.QLineEdit): The signal is emitted only if the text changed. """ + value_changed = QtC.pyqtSignal(str) def __init__(self, parent): @@ -159,6 +161,10 @@ def get_parameters_from_pickle(filename, obsid=None): if not catalogs: raise AperollException(f"No entries found in {filename}") + if obsid is None: + # this is ugly but it works whether the keys are strings of floats or ints + obsid = int(np.round(float(list(catalogs.keys())[0]))) + if float(obsid) not in catalogs: raise AperollException(f"OBSID {obsid} not found in {filename}") @@ -347,6 +353,9 @@ def set_parameters(self, **kwargs): ) else: params = get_default_parameters() + # obsid is a command-line argument, so I set it here + if "obsid" in kwargs: + params["obsid"] = kwargs["obsid"] logger.debug(pformat(params)) self.obsid_edit.setText(f"{params['obsid']}") From 8d69d7ae2e8fe83cf6a96ee7468cf1674d0e8f76 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 22 Oct 2024 20:11:16 -0400 Subject: [PATCH 20/20] ruff --- aperoll/widgets/parameters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aperoll/widgets/parameters.py b/aperoll/widgets/parameters.py index f4a1539..245577f 100644 --- a/aperoll/widgets/parameters.py +++ b/aperoll/widgets/parameters.py @@ -177,7 +177,9 @@ def get_parameters_from_pickle(filename, obsid=None): "dither_acq_z": catalog.dither_acq.z, "dither_guide_y": catalog.dither_guide.y, "dither_guide_z": catalog.dither_guide.z, - "date": CxoTime(catalog.date).date, # date is not guaranteed to be a fixed type in pickle + "date": CxoTime( + catalog.date + ).date, # date is not guaranteed to be a fixed type in pickle "attitude": catalog.att, "ra": catalog.att.ra, "dec": catalog.att.dec,