From 0304415c47880fa3f77815de8d0df5b77cb68955 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 15 Nov 2024 16:56:48 -0500 Subject: [PATCH 01/21] Added new QGRaphicsItems (FieldOfView, Centroid) - changed it so StarView.catalog is never None, and added Catalog.reset instead. - Added context menu to hide/show FOV, catalog and centroids. - Added auto-proseco feature --- aperoll/star_field_items.py | 298 +++++++++++++++++++++++++++- aperoll/utils.py | 5 +- aperoll/widgets/main_window.py | 9 + aperoll/widgets/star_plot.py | 346 +++++++++++++++++++++++++++++---- 4 files changed, 621 insertions(+), 37 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index e27e172..449d0a6 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -14,6 +14,8 @@ from PyQt5 import QtGui as QtG from PyQt5 import QtWidgets as QtW +from aperoll import utils + __all__ = [ "Star", "Catalog", @@ -22,6 +24,9 @@ "GuideStar", "AcqStar", "MonBox", + "Centroid", + "CameraOutline", + "FieldOfView", ] @@ -91,8 +96,20 @@ class Catalog(QtW.QGraphicsItem): separately for a given attitude. """ - def __init__(self, catalog, parent=None): + def __init__(self, catalog=None, parent=None): super().__init__(parent) + self.reset(catalog) + self.setZValue(50) + + def reset(self, catalog): + self.starcat = None + for item in self.childItems(): + item.setParentItem(None) + item.hide() + + if catalog is None: + return + self.starcat = catalog.copy() # will add some columns cat = Table(self.starcat) @@ -150,6 +167,39 @@ def __repr__(self): return repr(self.starcat) +class Centroid(QtW.QGraphicsEllipseItem): + def __init__(self, imgnum, parent=None): + self.imgnum = imgnum + self.excluded = False + s = 18 + w = 3 + rect = QtC.QRectF(-s, -s, 2 * s, 2 * s) + super().__init__(rect, parent) + color = QtG.QColor("blue") + pen = QtG.QPen(color, w) + self.setPen(pen) + + self._line_1 = QtW.QGraphicsLineItem(-s, 0, s, 0, self) + self._line_1.setPen(pen) + self._line_2 = QtW.QGraphicsLineItem(0, -s, 0, s, self) + self._line_2.setPen(pen) + + self._label = QtW.QGraphicsTextItem(f"{imgnum}", self) + self._label.setFont(QtG.QFont("Arial", 30)) + self._label.setDefaultTextColor(color) + self._label.setPos(30, -30) + + def set_excluded(self, excluded): + self.excluded = excluded + pen = self.pen() + color = pen.color() + color.setAlpha(85 if excluded else 255) + pen.setColor(color) + self.setPen(pen) + self._line_1.setPen(pen) + self._line_2.setPen(pen) + + class FidLight(QtW.QGraphicsEllipseItem): def __init__(self, fid, parent=None): self.starcat_row = fid @@ -218,3 +268,249 @@ def __init__(self, star, parent=None): super().__init__(-(hw * 2), -(hw * 2), hw * 4, hw * 4, parent) self.setPen(QtG.QPen(QtG.QColor(255, 165, 0), w)) self.setPos(star["row"], -star["col"]) + + +class FieldOfView(QtW.QGraphicsItem): + def __init__(self, attitude=None, alternate_outline=False): + super().__init__() + self.camera_outline = None + self.alternate_outline = alternate_outline + self.attitude = attitude + self.centroids = [Centroid(i, parent=self) for i in range(8)] + self._centroids = np.array( + [(0, 511, 511, 511, 511, 14, 0, 0, 0, 0) for _ in self.centroids], + dtype=[ + ("IMGNUM", int), + ("IMGROW0_8X8", float), + ("IMGCOL0_8X8", float), + ("IMGROW0", int), + ("IMGCOL0", int), + ("AOACMAG", float), + ("YAGS", float), + ("ZAGS", float), + ("IMGFID", bool), + ("excluded", bool), + ], + ) + self._centroids["IMGNUM"] = np.arange(8) + self.show_centroids = True + self.setZValue(80) + + def get_attitude(self): + return self._attitude + + def set_attitude(self, attitude): + if hasattr(self, "_attitude") and attitude == self._attitude: + return + self._attitude = attitude + if self.camera_outline is None: + self.camera_outline = CameraOutline(attitude, parent=self, simple=self.alternate_outline) + else: + self.camera_outline.attitude = attitude + if self.scene() is not None and self.scene().attitude is not None: + self.set_pos_for_attitude(self.scene().attitude) + + attitude = property(get_attitude, set_attitude) + + def set_pos_for_attitude(self, attitude): + if self.camera_outline is not None: + self.camera_outline.set_pos_for_attitude(attitude) + self._set_centroid_pos_for_attitude(attitude) + + def _set_centroid_pos_for_attitude(self, attitude): + yag, zag = self._centroids["YAGS"], self._centroids["ZAGS"] + row0, col0 = yagzag_to_pixels(yag, zag, allow_bad=True) + if attitude != self.attitude: + # the first attitude is the attitude of this frame, + # so we first get the ra/dec pointed to by the centroids + # and then calculate the position in the scene coordinate system + # for those ra/dec values + ra, dec = yagzag_to_radec(yag, zag, self.attitude) + yag, zag = radec_to_yagzag(ra, dec, attitude) + row, col = yagzag_to_pixels(yag, zag, allow_bad=True) + + off_ccd = ( + (row0 < -511) + | (row0 > 511) + | (col0 < -511) + | (col0 > 511) + | (self._centroids["IMGFID"]) + ) + for i, centroid in enumerate(self.centroids): + if off_ccd[i]: + centroid.setVisible(False) + else: + centroid.setVisible(self._centroids_visible) + centroid.setPos(row[i], -col[i]) + + def boundingRect(self): + return QtC.QRectF(0, 0, 1, 1) + + def paint(self, _painter, _option, _widget): + # this item draws nothing, it just holds children + pass + + def set_centroids(self, centroids): + missing_cols = set(centroids.dtype.names) - set(self._centroids.dtype.names) + if missing_cols: + raise ValueError(f"Missing columns in centroids: {missing_cols}") + + cols = list(centroids.dtype.names) + for col in cols: + self._centroids[col] = centroids[col] + self._set_centroid_pos_for_attitude(self.scene().attitude) + + def set_show_centroids(self, show=True): + self._centroids_visible = show + row, col = yagzag_to_pixels( + self._centroids["YAGS"], self._centroids["ZAGS"], allow_bad=True + ) + off_ccd = ( + (row < -511) + | (row > 511) + | (col < -511) + | (col > 511) + | (self._centroids["IMGFID"]) + ) + for i, centroid in enumerate(self.centroids): + if off_ccd[i]: + centroid.setVisible(False) + else: + centroid.setVisible(show) + + def get_show_centroids(self): + return self._centroids_visible + + show_centroids = property(get_show_centroids, set_show_centroids) + + def get_centroid_table(self): + self._centroids["MAG_ACA"] = [ + (centroid.excluded or not centroid.isVisible()) + for centroid in self.centroids + ] + + table = Table() + table["IMGNUM"] = self._centroids["IMGNUM"] + table["MAG_ACA"] = self._centroids["MAG_ACA"] + table["excluded"] = self._centroids["MAG_ACA"] + table["YAG"] = self._centroids["YAGS"] + table["ZAG"] = self._centroids["ZAGS"] + + return table + + +class CameraOutline(QtW.QGraphicsItem): + def __init__(self, attitude, parent=None, simple=False): + super().__init__(parent) + self.simple = simple + self._frame = utils.get_camera_fov_frame() + self.attitude = attitude + self.setZValue(100) + + def boundingRect(self): + # roughly half of the CCD size in arcsec (including some margin) + w = 2650 + return QtC.QRectF(-w, -w, 2 * w, 2 * w) + + def paint(self, painter, _option, _widget): + color = "lightGray" if self.simple else "black" + pen = QtG.QPen(QtG.QColor(color)) + pen.setWidth(1) + pen.setCosmetic(True) + + # I want to use antialising for these lines regardless of what is set for the scene, + # because they are large and otherwise look hideous. It will be reset at the end. + anti_aliasing_set = painter.testRenderHint(QtG.QPainter.Antialiasing) + painter.setRenderHint(QtG.QPainter.Antialiasing, True) + + painter.setPen(pen) + for i in range(len(self._frame["edge_1"]["row"]) - 1): + painter.drawLine( + QtC.QPointF( + self._frame["edge_1"]["row"][i], -self._frame["edge_1"]["col"][i] + ), + QtC.QPointF( + self._frame["edge_1"]["row"][i + 1], + -self._frame["edge_1"]["col"][i + 1], + ), + ) + if self.simple: + painter.setRenderHint(QtG.QPainter.Antialiasing, anti_aliasing_set) + return + + for i in range(len(self._frame["edge_2"]["row"]) - 1): + painter.drawLine( + QtC.QPointF( + self._frame["edge_2"]["row"][i], -self._frame["edge_2"]["col"][i] + ), + QtC.QPointF( + self._frame["edge_2"]["row"][i + 1], + -self._frame["edge_2"]["col"][i + 1], + ), + ) + + for i in range(len(self._frame["cross_2"]["row"]) - 1): + painter.drawLine( + QtC.QPointF( + self._frame["cross_2"]["row"][i], -self._frame["cross_2"]["col"][i] + ), + QtC.QPointF( + self._frame["cross_2"]["row"][i + 1], + -self._frame["cross_2"]["col"][i + 1], + ), + ) + for i in range(len(self._frame["cross_1"]["row"]) - 1): + painter.drawLine( + QtC.QPointF( + self._frame["cross_1"]["row"][i], -self._frame["cross_1"]["col"][i] + ), + QtC.QPointF( + self._frame["cross_1"]["row"][i + 1], + -self._frame["cross_1"]["col"][i + 1], + ), + ) + + painter.setRenderHint(QtG.QPainter.Antialiasing, anti_aliasing_set) + + def set_pos_for_attitude(self, attitude): + """ + Set the item position given the scene attitude. + + Note that the given attitude is NOT the attitude of the camera corresponding to this frame. + It's the origin of the scene coordinate system. + """ + if self._attitude is None: + raise Exception("FieldOfView attitude is not set. Can't set position.") + for key in self._frame: + # self._frame[key]["yag"] must not be modified + yag2, zag2 = radec_to_yagzag( + self._frame[key]["ra"], self._frame[key]["dec"], attitude + ) + self._frame[key]["row"], self._frame[key]["col"] = yagzag_to_pixels( + yag2, zag2, allow_bad=True + ) + self.update() + + def set_attitude(self, attitude): + """ + Set the attitude of the camera corresponding to this frame. + + Note that this is not the attitude of the scene coordinate system. + """ + if hasattr(self, "_attitude") and attitude == self._attitude: + return + self._attitude = attitude + if self._attitude is None: + for key in self._frame: + self._frame[key]["ra"] = None + self._frame[key]["dec"] = None + else: + for key in self._frame: + self._frame[key]["ra"], self._frame[key]["dec"] = yagzag_to_radec( + self._frame[key]["yag"], self._frame[key]["zag"], self._attitude + ) + + def get_attitude(self): + return self._attitude + + attitude = property(get_attitude, set_attitude) diff --git a/aperoll/utils.py b/aperoll/utils.py index e7ae757..af16491 100644 --- a/aperoll/utils.py +++ b/aperoll/utils.py @@ -1,5 +1,8 @@ import numpy as np -from chandra_aca.transform import pixels_to_yagzag, yagzag_to_pixels +from chandra_aca.transform import ( + pixels_to_yagzag, + yagzag_to_pixels, +) from ska_helpers import logging logger = logging.basic_logger("aperoll") diff --git a/aperoll/widgets/main_window.py b/aperoll/widgets/main_window.py index f44d900..273c55e 100644 --- a/aperoll/widgets/main_window.py +++ b/aperoll/widgets/main_window.py @@ -148,6 +148,7 @@ def __init__(self, opts=None): # noqa: PLR0915 # self.plot.exclude_star.connect(self.parameters.exclude_star) self.parameters.do_it.connect(self._run_proseco) + self.plot.update_proseco.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) @@ -181,6 +182,11 @@ def __init__(self, opts=None): # noqa: PLR0915 if starcat is not None: self.plot.set_catalog(starcat) self.starcat_view.set_catalog(aca) + # make sure the catalog is not overwritten automatically + self.plot.scene.state.auto_proseco = False + + if self.plot.scene.state.auto_proseco: + self._run_proseco() def closeEvent(self, event): if self.web_page is not None: @@ -192,6 +198,8 @@ def _parameters_changed(self): proseco_args = self.parameters.proseco_args() self.plot.set_base_attitude(proseco_args["att"]) self._data.reset(proseco_args) + if self.plot.scene.state.auto_proseco and not self.plot.view.moving: + self._run_proseco() def _init(self): if self.parameters.values: @@ -203,6 +211,7 @@ def _init(self): ) self.plot.set_base_attitude(aca_attitude) self.plot.set_time(time) + self.plot.scene.state = "Proseco" def _reset(self): self.parameters.set_parameters(**self.opts) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index d00d689..9e4fc1d 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + import agasc import numpy as np import tables @@ -14,11 +16,72 @@ from Quaternion import Quat from aperoll import utils -from aperoll.star_field_items import Catalog, Star +from aperoll.star_field_items import Catalog, Centroid, FieldOfView, Star + + +@dataclass +class State: + """ + Dataclass to hold the state of the star field. + + The state includes all flags that determine the visibility of different elements and the + behavior of the star field. + """ + + name: str = "" + # regular attitude updates are used to set `attitude`, `alternate_attitude` or None. + onboard_attitude_slot: str | None = "attitude" + enable_fov: bool = True + enable_alternate_fov: bool = False + enable_catalog: bool = False + enable_centroids: bool = True + enable_alternate_fov_centroids: bool = False + auto_proseco: bool = False + + +_COMMON_STATES = [ + { + # use case: real-time telemetry. + # (attitude set to on-board attitude, catalog off, centroids on) + "name": "Telemetry", + "onboard_attitude_slot": "attitude", + "enable_fov": True, + "enable_alternate_fov": False, + "enable_catalog": False, + "enable_centroids": True, + "enable_alternate_fov_centroids": False, + "auto_proseco": False, + }, + { + # use case: starndard aperoll use. + # (on-board attitude never updated, catalog on, centroids off, auto-proseco) + "name": "Proseco", + "onboard_attitude_slot": None, + "enable_fov": True, + "enable_alternate_fov": False, + "enable_catalog": True, + "enable_centroids": False, + "enable_alternate_fov_centroids": False, + "auto_proseco": True, + }, + { + # use case: "alternate" real-time telemetry + # (attitude se to user attitude, but showing the camera outline in the alternate FOV) + "name": "Alternate", + "onboard_attitude_slot": "alternate_attitude", + "enable_fov": True, + "enable_alternate_fov": True, + "enable_catalog": False, + "enable_centroids": False, + "enable_alternate_fov_centroids": True, + "auto_proseco": False, + }, +] class StarView(QtW.QGraphicsView): include_star = QtC.pyqtSignal(int, str, object) + update_proseco = QtC.pyqtSignal() def __init__(self, scene=None): super().__init__(scene) @@ -32,7 +95,22 @@ def __init__(self, scene=None): self._rotating = False self._moving = False - self._draw_frame = True + self._draw_frame = False + + application = QtW.QApplication.instance() + main_windows = [ + w for w in application.topLevelWidgets() if isinstance(w, QtW.QMainWindow) + ] + for window in main_windows: + menu_bar = window.menuBar() + menu = menu_bar.addMenu("Aperoll") + menu = menu.addMenu("Star Field Quick Config") + for state in self.scene().states.values(): + action = QtW.QAction(state.name, self) + action.triggered.connect( + lambda _checked, name=state.name: self.scene().set_state(name) + ) + menu.addAction(action) def _get_draw_frame(self): return self._draw_frame @@ -65,6 +143,8 @@ def mouseMoveEvent(self, event): self._moving = True if self._moving or self._rotating: + if self.scene().onboard_attitude_slot == "attitude": + self.scene().onboard_attitude_slot = "alternate_attitude" end_pos = self.mapToScene(pos) start_pos = self.mapToScene(self._start) if self._moving: @@ -86,6 +166,8 @@ def mouseMoveEvent(self, event): def mouseReleaseEvent(self, event): if event.button() == QtC.Qt.LeftButton: self._start = None + if (self._moving or self._rotating) and self.scene().state.auto_proseco: + self.update_proseco.emit() def mousePressEvent(self, event): if event.button() == QtC.Qt.LeftButton: @@ -93,6 +175,11 @@ def mousePressEvent(self, event): self._rotating = False self._start = event.pos() + def get_moving(self): + return self._moving or self._rotating + + moving = property(get_moving) + def wheelEvent(self, event): scale = 1 + 0.5 * event.angleDelta().y() / 360 if scale < 0: @@ -151,41 +238,101 @@ def drawForeground(self, painter, _rect): ) painter.setRenderHint(QtG.QPainter.Antialiasing, anti_aliasing_set) - def contextMenuEvent(self, event): - items = [item for item in self.items(event.pos()) if isinstance(item, Star)] - if not items: - return - item = items[0] + def contextMenuEvent(self, event): # noqa: PLR0912, PLR0915 + stars = [item for item in self.items(event.pos()) if isinstance(item, Star)] + star = stars[0] if stars else None + + centroids = [ + item for item in self.items(event.pos()) if isinstance(item, Centroid) + ] + centroid = centroids[0] if centroids else None menu = QtW.QMenu() - incl_action = QtW.QAction("include acq", menu, checkable=True) - incl_action.setChecked(item.included["acq"] is True) - menu.addAction(incl_action) + if star is not None: + incl_action = QtW.QAction("include acq", menu, checkable=True) + incl_action.setChecked(star.included["acq"] is True) + menu.addAction(incl_action) + + excl_action = QtW.QAction("exclude acq", menu, checkable=True) + excl_action.setChecked(star.included["acq"] is False) + menu.addAction(excl_action) + + incl_action = QtW.QAction("include guide", menu, checkable=True) + incl_action.setChecked(star.included["guide"] is True) + menu.addAction(incl_action) + + excl_action = QtW.QAction("exclude guide", menu, checkable=True) + excl_action.setChecked(star.included["guide"] is False) + menu.addAction(excl_action) + + if centroid is not None: + incl_action = QtW.QAction( + f"include slot {centroid.imgnum}", menu, checkable=True + ) + incl_action.setChecked(not centroid.excluded) + menu.addAction(incl_action) + + show_catalog_action = QtW.QAction("Show Catalog", menu, checkable=True) + show_catalog_action.setChecked(self.scene().state.enable_catalog) + menu.addAction(show_catalog_action) + + show_fov_action = QtW.QAction("Show Alt FOV", menu, checkable=True) + show_fov_action.setChecked(self.scene().state.enable_alternate_fov) + menu.addAction(show_fov_action) + + show_alt_fov_action = QtW.QAction("Show FOV", menu, checkable=True) + show_alt_fov_action.setChecked(self.scene().state.enable_fov) + menu.addAction(show_alt_fov_action) - excl_action = QtW.QAction("exclude acq", menu, checkable=True) - excl_action.setChecked(item.included["acq"] is False) - menu.addAction(excl_action) + show_centroids_action = QtW.QAction("Show Centroids", menu, checkable=True) + show_centroids_action.setChecked(self.scene().main_fov.show_centroids) + menu.addAction(show_centroids_action) - incl_action = QtW.QAction("include guide", menu, checkable=True) - incl_action.setChecked(item.included["guide"] is True) - menu.addAction(incl_action) + show_alt_centroids_action = QtW.QAction( + "Show Alt Centroids", menu, checkable=True + ) + show_alt_centroids_action.setChecked(self.scene().alternate_fov.show_centroids) + menu.addAction(show_alt_centroids_action) + + set_auto_proseco_action = QtW.QAction("Auto-proseco", menu, checkable=True) + set_auto_proseco_action.setChecked(self.scene().state.auto_proseco) + menu.addAction(set_auto_proseco_action) - excl_action = QtW.QAction("exclude guide", menu, checkable=True) - excl_action.setChecked(item.included["guide"] is False) - menu.addAction(excl_action) + reset_fov_action = QtW.QAction("Reset Alt FOV", menu, checkable=False) + menu.addAction(reset_fov_action) result = menu.exec_(event.globalPos()) if result is not None: - action, action_type = result.text().split() - if items: + if centroid is not None and result.text().startswith("include slot"): + centroid.set_excluded(not result.isChecked()) + elif stars and result.text().split()[0] in ["include", "exclude"]: + action, action_type = result.text().split() if action == "include": - item.included[action_type] = True if result.isChecked() else None + star.included[action_type] = True if result.isChecked() else None elif action == "exclude": - item.included[action_type] = False if result.isChecked() else None + star.included[action_type] = False if result.isChecked() else None self.include_star.emit( - item.star["AGASC_ID"], action_type, item.included[action_type] + star.star["AGASC_ID"], action_type, star.included[action_type] ) + elif result.text() == "Show FOV": + self.scene().enable_fov(result.isChecked()) + elif result.text() == "Show Alt FOV": + self.scene().enable_alternate_fov(result.isChecked()) + elif result.text() == "Show Catalog": + self.scene().enable_catalog(result.isChecked()) + elif result.text() == "Show Centroids": + self.scene().main_fov.show_centroids = result.isChecked() + elif result.text() == "Show Alt Centroids": + self.scene().alternate_fov.show_centroids = result.isChecked() + elif result.text() == "Reset Alt FOV": + self.scene().alternate_fov.set_attitude(self.scene().attitude) + elif result.text() == "Auto-proseco": + self.scene().state.auto_proseco = result.isChecked() + if result.isChecked(): + self.update_proseco.emit() + else: + print(f"Action {result.text()} not implemented") event.accept() def resizeEvent(self, event): @@ -207,8 +354,6 @@ def set_visibility(self): side = max(np.abs(tl.x() - br.x()), np.abs(tl.y() - br.y())) radius = 1.5 * (side / 2) * 5 / 3600 # 5 arcsec per pixel, radius in degree - self.draw_frame = radius < 6 - r = agasc.sphere_dist( self.scene().attitude.ra, self.scene().attitude.dec, @@ -224,6 +369,9 @@ def set_visibility(self): else: item["graphic_item"].show() + self.scene().show_fov(radius < 6) + self.scene().show_alternate_fov(radius < 6) + def set_item_scale(self): if self.scene()._stars is not None: # when zooming out (scaling < 1), the graphic items should not get too small @@ -258,6 +406,7 @@ def scale(self, sx, sy): class StarField(QtW.QGraphicsScene): attitude_changed = QtC.pyqtSignal() + state_changed = QtC.pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) @@ -265,11 +414,51 @@ def __init__(self, parent=None): self._attitude = None self._time = None self._stars = None - self._catalog = None + + self.catalog = Catalog() + self.addItem(self.catalog) self._healpix_indices = set() self._update_radius = 2 + # alternate fov added first so it never hides the main fov + self.alternate_fov = FieldOfView(alternate_outline=True) + self.addItem(self.alternate_fov) + + self.main_fov = FieldOfView() + self.addItem(self.main_fov) + + self.states = {value["name"]: State(**value) for value in _COMMON_STATES} + self.set_state("Telemetry") + if self.state.auto_proseco: + self.update_proseco() + + def get_onboard_attitude_slot(self): + return self.state.onboard_attitude_slot + + def set_onboard_attitude_slot(self, attitude): + self.state.onboard_attitude_slot = attitude + + onboard_attitude_slot = property( + get_onboard_attitude_slot, set_onboard_attitude_slot + ) + + def get_state(self): + return self._state + + def set_state(self, state_name): + self._state = self.states[state_name] + + self.enable_fov(self._state.enable_fov) + self.enable_alternate_fov(self._state.enable_alternate_fov) + self.enable_catalog(self._state.enable_catalog) + self.alternate_fov.show_centroids = self._state.enable_alternate_fov_centroids + self.main_fov.show_centroids = self._state.enable_centroids + + self.state_changed.emit(state_name) + + state = property(get_state, set_state) + def update_stars(self, radius=None): if radius is not None: self._update_radius = radius @@ -408,14 +597,29 @@ def get_time(self): time = property(get_time, set_time) + def add_catalog(self, starcat): + self.catalog.reset(starcat) + def set_attitude(self, attitude): """ Set the attitude of the scene, rotating the items to the given attitude. """ if attitude != self._attitude: - if self._catalog is not None: - self._catalog.set_pos_for_attitude(attitude) + if self.catalog is not None: + self.catalog.set_pos_for_attitude(attitude) + + # the main FOV is always at the base attitude + self.main_fov.set_attitude(attitude) + self.main_fov.set_pos_for_attitude(attitude) + if self.alternate_fov.attitude is None: + # by default the alternate FOV is at the initial base attitude. + self.alternate_fov.set_attitude(attitude) + if self.alternate_fov.attitude is not None: + # after the first time, the alternate FOV is at the same attitude + # it is just moved around. + self.alternate_fov.set_pos_for_attitude(attitude) + self._attitude = attitude self.update_stars() self.attitude_changed.emit() @@ -425,17 +629,60 @@ def get_attitude(self): attitude = property(get_attitude, set_attitude) - def add_catalog(self, starcat): - if self._catalog is not None: - self.removeItem(self._catalog) + def set_alternate_attitude(self, attitude=None): + self.alternate_fov.set_attitude( + attitude if attitude is not None else self.attitude + ) + + def get_alternate_attitude(self): + return self.alternate_fov.attitude + + alternate_attitude = property(get_alternate_attitude, set_alternate_attitude) + + def set_onboard_attitude(self, attitude): + """ + Set the on-board attitude from telemetry. + + Sometimes the attitude from telemetry is not the same as the base attitude. + """ + if self.onboard_attitude_slot == "attitude": + self.attitude = attitude + elif self.onboard_attitude_slot == "alternate_attitude": + self.alternate_attitude = attitude - self._catalog = Catalog(starcat) - self.addItem(self._catalog) + def enable_alternate_fov(self, enable=True): + self.state.enable_alternate_fov = enable + self.alternate_fov.setVisible(enable) + + def show_alternate_fov(self, show=True): + if self.state.enable_alternate_fov: + self.alternate_fov.setVisible(show) + + def enable_fov(self, enable=True): + self.state.enable_fov = enable + self.main_fov.setVisible(enable) + + def show_fov(self, show=True): + if self.state.enable_fov: + self.main_fov.setVisible(show) + + def set_centroids(self, centroids): + self.main_fov.set_centroids(centroids) + self.alternate_fov.set_centroids(centroids) + + def enable_catalog(self, enable=True): + self.state.enable_catalog = enable + self.catalog.setVisible(enable) + + def show_catalog(self, show=True): + if self.state.enable_catalog: + self.catalog.setVisible(show) class StarPlot(QtW.QWidget): attitude_changed = QtC.pyqtSignal(float, float, float) include_star = QtC.pyqtSignal(int, str, object) + update_proseco = QtC.pyqtSignal() def __init__(self, parent=None): super().__init__(parent) @@ -463,6 +710,7 @@ def __init__(self, parent=None): self.scene.changed.connect(self.view.set_visibility) self.view.include_star.connect(self.include_star) + self.view.update_proseco.connect(self.update_proseco) def _attitude_changed(self): if self.scene.attitude is not None: @@ -480,6 +728,12 @@ def set_base_attitude(self, q): """ self.scene.set_attitude(q) + def set_onboard_attitude(self, attitude): + """ + Set the on-board attitude from telemetry. + """ + self.scene.set_onboard_attitude(attitude) + def set_time(self, t): self._time = CxoTime(t) self.scene.time = self._time @@ -488,13 +742,35 @@ def highlight(self, agasc_ids): self._highlight = agasc_ids def set_catalog(self, catalog): + if ( + self._catalog is not None + and (len(self._catalog) == len(catalog)) + and np.all(self._catalog == catalog) + ): + return + # setting the time might not be exactly right in general, because time can come directly + # from telemetry, and the catalog date might be much earlier, but this is needed for the + # standard aperoll use case. In telemetry mode, the difference will not matter. self.set_time(catalog.date) self._catalog = catalog self.show_catalog() - def show_catalog(self): + def show_catalog(self, show=True): if self._catalog is not None: self.scene.add_catalog(self._catalog) + self.scene.show_catalog(show) + + def set_alternate_attitude(self, attitude=None): + self.scene.set_alternate_attitude(attitude) + + def show_alternate_fov(self, show=True): + self.scene.show_alternate_fov(show) + + def show_fov(self, show=True): + self.scene.show_fov(show) + + def set_centroids(self, centroids): + self.scene.set_centroids(centroids) def main(): From 7db1073ab9da236731212d5b7dda1127900021c8 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 21 Nov 2024 13:03:13 -0500 Subject: [PATCH 02/21] typo --- aperoll/widgets/star_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 9e4fc1d..930e858 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -66,7 +66,7 @@ class State: }, { # use case: "alternate" real-time telemetry - # (attitude se to user attitude, but showing the camera outline in the alternate FOV) + # (attitude set to user attitude, but showing the camera outline in the alternate FOV) "name": "Alternate", "onboard_attitude_slot": "alternate_attitude", "enable_fov": True, From 17812ba9ccafb56dca90af27cf65e06bdc5efd56 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 21 Nov 2024 13:03:25 -0500 Subject: [PATCH 03/21] rename State -> StarFieldState --- aperoll/widgets/star_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 930e858..43a73e8 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -20,7 +20,7 @@ @dataclass -class State: +class StarFieldState: """ Dataclass to hold the state of the star field. @@ -428,7 +428,7 @@ def __init__(self, parent=None): self.main_fov = FieldOfView() self.addItem(self.main_fov) - self.states = {value["name"]: State(**value) for value in _COMMON_STATES} + self.states = {value["name"]: StarFieldState(**value) for value in _COMMON_STATES} self.set_state("Telemetry") if self.state.auto_proseco: self.update_proseco() From ac44f9d01a06b2871b1125bdcc8d200b3825a116 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 21 Nov 2024 13:06:14 -0500 Subject: [PATCH 04/21] ruff --- aperoll/star_field_items.py | 4 +++- aperoll/widgets/star_plot.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 449d0a6..3700749 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -304,7 +304,9 @@ def set_attitude(self, attitude): return self._attitude = attitude if self.camera_outline is None: - self.camera_outline = CameraOutline(attitude, parent=self, simple=self.alternate_outline) + self.camera_outline = CameraOutline( + attitude, parent=self, simple=self.alternate_outline + ) else: self.camera_outline.attitude = attitude if self.scene() is not None and self.scene().attitude is not None: diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 43a73e8..584cc14 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -428,7 +428,9 @@ def __init__(self, parent=None): self.main_fov = FieldOfView() self.addItem(self.main_fov) - self.states = {value["name"]: StarFieldState(**value) for value in _COMMON_STATES} + self.states = { + value["name"]: StarFieldState(**value) for value in _COMMON_STATES + } self.set_state("Telemetry") if self.state.auto_proseco: self.update_proseco() From f8cb0a4fce89f05266f210402e23a88ee3681129 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 21 Nov 2024 13:18:57 -0500 Subject: [PATCH 05/21] fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aperoll/widgets/star_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 584cc14..260f613 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -53,7 +53,7 @@ class StarFieldState: "auto_proseco": False, }, { - # use case: starndard aperoll use. + # use case: standard aperoll use. # (on-board attitude never updated, catalog on, centroids off, auto-proseco) "name": "Proseco", "onboard_attitude_slot": None, From 53ac8848867e42d5b87681809e7c3b3ded2c0130 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 21 Nov 2024 13:59:33 -0500 Subject: [PATCH 06/21] change line color in camera outline --- aperoll/star_field_items.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 3700749..ca4ae4f 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -451,6 +451,10 @@ def paint(self, painter, _option, _widget): ), ) + magenta_pen = QtG.QPen(QtG.QColor("magenta")) + magenta_pen.setCosmetic(True) + magenta_pen.setWidth(1) + painter.setPen(magenta_pen) for i in range(len(self._frame["cross_2"]["row"]) - 1): painter.drawLine( QtC.QPointF( From 0f88fbc20b09308298cf684a0b268f3498afca2b Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 12:48:13 -0500 Subject: [PATCH 07/21] factor out position calculation from graphic items --- aperoll/star_field_items.py | 173 ++++++++++++++++++++++------------- aperoll/widgets/star_plot.py | 20 ++-- 2 files changed, 116 insertions(+), 77 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index ca4ae4f..c253da5 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -17,6 +17,7 @@ from aperoll import utils __all__ = [ + "star_field_position", "Star", "Catalog", "FidLight", @@ -30,6 +31,59 @@ ] +def star_field_position( + attitude=None, + yag=None, + zag=None, + ra=None, + dec=None, + row=None, + col=None, + ): + """ + Calculate the position of an item in the star_field. + + This function completely determines the position of an item in the star field, given the + attitude. Some items' positions are determined by yag/zag (like centroids), others' are + determined by RA/Dec (like stars), and others are given in pixels (like bad pixels). + + Catalog elements (guide/acq/fids) use RA/Dec to make sure they always point to the star in the + catalog even when the attitude changes. + + Parameters + ---------- + attitude : Quaternion, optional + The attitude of the scene. This is required if the position is given by RA/dec. + yag : float, optional + The Y angle in degrees. + zag : float, optional + The Z angle in degrees. + ra : float, optional + The RA in degrees. + dec : float, optional + The Dec in degrees. + row : float, optional + The row in pixels. + col : float, optional + The column in pixels. + + Returns + ------- + x, y : float + The position of the item in the scene. + """ + if ra is not None and dec is not None: + if attitude is None: + raise ValueError("Attitude must be given if RA/Dec is given.") + yag, zag = radec_to_yagzag(ra, dec, attitude) + if yag is not None and zag is not None: + row, col = yagzag_to_pixels(yag, zag, allow_bad=True) + if row is None or col is None: + raise ValueError("Either YAG/ZAG, RA/Dec or row/col must be given.") + # note that the coordinate system is (row, -col), which is (-yag, -zag) + return row, -col + + def symsize(mag): # map mags to figsizes, defining # mag 6 as 40 and mag 11 as 3 @@ -113,11 +167,12 @@ def reset(self, catalog): self.starcat = catalog.copy() # will add some columns cat = Table(self.starcat) - # item positions are set from row/col - cat["row"], cat["col"] = yagzag_to_pixels( - cat["yang"], cat["zang"], allow_bad=True - ) - # when attitude changes, the positions (row, col) are recalculated from (ra, dec) + + # If the attitude changes (e.g. if we rotate the star field, changing the roll angle) + # the yang/zang values will not be the same as the ones originally in the catalog, + # because the corresponding star moved in the CCD. To keep track of that, we project back + # to get the corresponding RA/dec values assuming the attitude in the catalog. + # Later, when attitude changes, the positions are recalculated from (ra, dec) # so these items move with the corresponding star. cat["ra"], cat["dec"] = yagzag_to_radec( cat["yang"], cat["zang"], self.starcat.att @@ -133,6 +188,8 @@ def reset(self, catalog): self.mon_stars = [MonBox(mon_box, self) for mon_box in mon_wins] self.fid_lights = [FidLight(fid, self) for fid in fids] + self.set_pos_for_attitude(self.scene().attitude) + def setPos(self, *_args, **_kwargs): # the position of the catalog is ALLWAYS (0,0) pass @@ -149,12 +206,12 @@ def set_pos_for_attitude(self, attitude): # item positions are relative to the parent's position (self) # but the parent's position is (or should be) always (0, 0) for item in self.childItems(): - yag, zag = radec_to_yagzag( - item.starcat_row["ra"], item.starcat_row["dec"], attitude + x, y = star_field_position( + attitude, + ra=item.starcat_row["ra"], + dec=item.starcat_row["dec"], ) - row, col = yagzag_to_pixels(yag, zag, allow_bad=True) - # item.setPos(-yag, -zag) - item.setPos(row, -col) + item.setPos(x, y) def boundingRect(self): return QtC.QRectF(0, 0, 1, 1) @@ -210,8 +267,6 @@ def __init__(self, fid, parent=None): self.fid = fid pen = QtG.QPen(QtG.QColor("red"), w) self.setPen(pen) - # self.setPos(-fid["yang"], -fid["zang"]) - self.setPos(fid["row"], -fid["col"]) line = QtW.QGraphicsLineItem(-s, 0, s, 0, self) line.setPen(pen) @@ -226,8 +281,6 @@ def __init__(self, star, parent=None): self._offset = 30 self.setFont(QtG.QFont("Arial", 30)) self.setDefaultTextColor(QtG.QColor("red")) - # self.setPos(-star["yang"], -star["zang"]) - self.setPos(star["row"], -star["col"]) def setPos(self, x, y): rect = self.boundingRect() @@ -244,9 +297,6 @@ def __init__(self, star, parent=None): rect = QtC.QRectF(-s, -s, s * 2, s * 2) super().__init__(rect, parent) self.setPen(QtG.QPen(QtG.QColor("green"), w)) - # self.setPos(-star["yang"], -star["zang"]) - self.setPos(star["row"], -star["col"]) - class AcqStar(QtW.QGraphicsRectItem): def __init__(self, star, parent=None): @@ -255,9 +305,6 @@ def __init__(self, star, parent=None): w = 5 super().__init__(-hw, -hw, hw * 2, hw * 2, parent) self.setPen(QtG.QPen(QtG.QColor("blue"), w)) - # self.setPos(-star["yang"], -star["zang"]) - self.setPos(star["row"], -star["col"]) - class MonBox(QtW.QGraphicsRectItem): def __init__(self, star, parent=None): @@ -267,7 +314,6 @@ def __init__(self, star, parent=None): w = 5 super().__init__(-(hw * 2), -(hw * 2), hw * 4, hw * 4, parent) self.setPen(QtG.QPen(QtG.QColor(255, 165, 0), w)) - self.setPos(star["row"], -star["col"]) class FieldOfView(QtW.QGraphicsItem): @@ -321,29 +367,20 @@ def set_pos_for_attitude(self, attitude): def _set_centroid_pos_for_attitude(self, attitude): yag, zag = self._centroids["YAGS"], self._centroids["ZAGS"] - row0, col0 = yagzag_to_pixels(yag, zag, allow_bad=True) - if attitude != self.attitude: - # the first attitude is the attitude of this frame, - # so we first get the ra/dec pointed to by the centroids - # and then calculate the position in the scene coordinate system - # for those ra/dec values + if attitude == self.attitude: + x, y = star_field_position(yag=yag, zag=zag) + else: + # `self.attitude` is the attitude represented by this field of view, but the scene's + # attitude is `attitude`. We first project back to get the ra/dec pointed to by the + # centroids using `self.attitude`, and calculate the position in the scene coordinate + # system using those ra/dec values ra, dec = yagzag_to_radec(yag, zag, self.attitude) - yag, zag = radec_to_yagzag(ra, dec, attitude) - row, col = yagzag_to_pixels(yag, zag, allow_bad=True) + x, y = star_field_position(attitude=attitude, ra=ra, dec=dec) + + self.set_show_centroids(self._centroids_visible) - off_ccd = ( - (row0 < -511) - | (row0 > 511) - | (col0 < -511) - | (col0 > 511) - | (self._centroids["IMGFID"]) - ) for i, centroid in enumerate(self.centroids): - if off_ccd[i]: - centroid.setVisible(False) - else: - centroid.setVisible(self._centroids_visible) - centroid.setPos(row[i], -col[i]) + centroid.setPos(x[i], y[i]) def boundingRect(self): return QtC.QRectF(0, 0, 1, 1) @@ -364,6 +401,7 @@ def set_centroids(self, centroids): def set_show_centroids(self, show=True): self._centroids_visible = show + # centroids are hidden if they fall outside the CCD row, col = yagzag_to_pixels( self._centroids["YAGS"], self._centroids["ZAGS"], allow_bad=True ) @@ -415,6 +453,13 @@ def boundingRect(self): return QtC.QRectF(-w, -w, 2 * w, 2 * w) def paint(self, painter, _option, _widget): + if "x" not in self._frame["edge_1"]: + if self.scene() is not None and self.scene().attitude is not None: + self.set_pos_for_attitude(self.scene().attitude) + else: + # attitude has not been set, not drawing + return + color = "lightGray" if self.simple else "black" pen = QtG.QPen(QtG.QColor(color)) pen.setWidth(1) @@ -426,28 +471,28 @@ def paint(self, painter, _option, _widget): painter.setRenderHint(QtG.QPainter.Antialiasing, True) painter.setPen(pen) - for i in range(len(self._frame["edge_1"]["row"]) - 1): + for i in range(len(self._frame["edge_1"]["x"]) - 1): painter.drawLine( QtC.QPointF( - self._frame["edge_1"]["row"][i], -self._frame["edge_1"]["col"][i] + self._frame["edge_1"]["x"][i], self._frame["edge_1"]["y"][i] ), QtC.QPointF( - self._frame["edge_1"]["row"][i + 1], - -self._frame["edge_1"]["col"][i + 1], + self._frame["edge_1"]["x"][i + 1], + self._frame["edge_1"]["y"][i + 1], ), ) if self.simple: painter.setRenderHint(QtG.QPainter.Antialiasing, anti_aliasing_set) return - for i in range(len(self._frame["edge_2"]["row"]) - 1): + for i in range(len(self._frame["edge_2"]["x"]) - 1): painter.drawLine( QtC.QPointF( - self._frame["edge_2"]["row"][i], -self._frame["edge_2"]["col"][i] + self._frame["edge_2"]["x"][i], self._frame["edge_2"]["y"][i] ), QtC.QPointF( - self._frame["edge_2"]["row"][i + 1], - -self._frame["edge_2"]["col"][i + 1], + self._frame["edge_2"]["x"][i + 1], + self._frame["edge_2"]["y"][i + 1], ), ) @@ -455,24 +500,24 @@ def paint(self, painter, _option, _widget): magenta_pen.setCosmetic(True) magenta_pen.setWidth(1) painter.setPen(magenta_pen) - for i in range(len(self._frame["cross_2"]["row"]) - 1): + for i in range(len(self._frame["cross_2"]["x"]) - 1): painter.drawLine( QtC.QPointF( - self._frame["cross_2"]["row"][i], -self._frame["cross_2"]["col"][i] + self._frame["cross_2"]["x"][i], self._frame["cross_2"]["y"][i] ), QtC.QPointF( - self._frame["cross_2"]["row"][i + 1], - -self._frame["cross_2"]["col"][i + 1], + self._frame["cross_2"]["x"][i + 1], + self._frame["cross_2"]["y"][i + 1], ), ) - for i in range(len(self._frame["cross_1"]["row"]) - 1): + for i in range(len(self._frame["cross_1"]["x"]) - 1): painter.drawLine( QtC.QPointF( - self._frame["cross_1"]["row"][i], -self._frame["cross_1"]["col"][i] + self._frame["cross_1"]["x"][i], self._frame["cross_1"]["y"][i] ), QtC.QPointF( - self._frame["cross_1"]["row"][i + 1], - -self._frame["cross_1"]["col"][i + 1], + self._frame["cross_1"]["x"][i + 1], + self._frame["cross_1"]["y"][i + 1], ), ) @@ -482,19 +527,19 @@ def set_pos_for_attitude(self, attitude): """ Set the item position given the scene attitude. - Note that the given attitude is NOT the attitude of the camera corresponding to this frame. + Note that the given attitude is NOT the attitude of the camera represented by this outline. It's the origin of the scene coordinate system. """ if self._attitude is None: raise Exception("FieldOfView attitude is not set. Can't set position.") + for key in self._frame: - # self._frame[key]["yag"] must not be modified - yag2, zag2 = radec_to_yagzag( - self._frame[key]["ra"], self._frame[key]["dec"], attitude - ) - self._frame[key]["row"], self._frame[key]["col"] = yagzag_to_pixels( - yag2, zag2, allow_bad=True + self._frame[key]["x"], self._frame[key]["y"] = star_field_position( + attitude, + ra=self._frame[key]["ra"], + dec=self._frame[key]["dec"], ) + self.update() def set_attitude(self, attitude): diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 260f613..9b3e608 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -5,10 +5,6 @@ import tables from astropy import units as u from astropy.table import Table, vstack -from chandra_aca.transform import ( - radec_to_yagzag, - yagzag_to_pixels, -) from cxotime import CxoTime from PyQt5 import QtCore as QtC from PyQt5 import QtGui as QtG @@ -16,7 +12,7 @@ from Quaternion import Quat from aperoll import utils -from aperoll.star_field_items import Catalog, Centroid, FieldOfView, Star +from aperoll.star_field_items import Catalog, Centroid, FieldOfView, Star, star_field_position @dataclass @@ -579,15 +575,13 @@ def set_star_positions(self): if self._stars is not None and self._attitude is not None: # The calculation of row/col is done here so it can be vectorized # if done for each item, it is much slower. - self._stars["yang"], self._stars["zang"] = radec_to_yagzag( - self._stars["RA_PMCORR"], self._stars["DEC_PMCORR"], self._attitude + x, y = star_field_position( + self._attitude, + ra=self._stars["RA_PMCORR"], + dec=self._stars["DEC_PMCORR"] ) - self._stars["row"], self._stars["col"] = yagzag_to_pixels( - self._stars["yang"], self._stars["zang"], allow_bad=True - ) - for item in self._stars: - # note that the coordinate system is (row, -col), which is (-yag, -zag) - item["graphic_item"].setPos(item["row"], -item["col"]) + for idx, item in enumerate(self._stars): + item["graphic_item"].setPos(x[idx], y[idx]) def set_time(self, time): if time != self._time: From 2e278fda6dc2106cf89b2b9061d5ad2e2ecf56c6 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 12:54:32 -0500 Subject: [PATCH 08/21] remove code that is never run (and would fail) --- aperoll/widgets/star_plot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 9b3e608..148746f 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -428,8 +428,6 @@ def __init__(self, parent=None): value["name"]: StarFieldState(**value) for value in _COMMON_STATES } self.set_state("Telemetry") - if self.state.auto_proseco: - self.update_proseco() def get_onboard_attitude_slot(self): return self.state.onboard_attitude_slot From 0d78dd579df6c07237cf9219cfb9a5bb37fe76e7 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 13:39:51 -0500 Subject: [PATCH 09/21] docstrings --- aperoll/star_field_items.py | 163 ++++++++++++++++++++++++++++++++++- aperoll/widgets/star_plot.py | 2 +- 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index c253da5..2b13254 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -85,6 +85,9 @@ def star_field_position( def symsize(mag): + """ + Symbol size for a star of a given magnitude. + """ # map mags to figsizes, defining # mag 6 as 40 and mag 11 as 3 # interp should leave it at the bounding value outside @@ -93,6 +96,28 @@ def symsize(mag): class Star(QtW.QGraphicsEllipseItem): + """ + QGraphicsItem representing a star. + + Stars are depicted as circles, and the color is automatically set based on the magnitude + (faint stars are gray) and whether the star is an acquisition or guide star candidate: + + - If the star is maked as "highlighted" it is drawn bright red. + - Faint stars (mag > 10.5 are light gray). + - Stars that are not acquisition of guide candidates are tomato red. + - All others are black. + + This class also handles the tooltip that shows up when one hovers over the star. + + Parameters + ---------- + star : astropy.table.Row + One row from the AGASC table. + parent : QGraphicsItem, optional + The parent item. + highlight : bool, optional + If True, the star is highlighted in red. + """ def __init__(self, star, parent=None, highlight=False): s = symsize(star["MAG_ACA"]) rect = QtC.QRectF(-s / 2, -s / 2, s, s) @@ -148,6 +173,11 @@ class Catalog(QtW.QGraphicsItem): Note that the position of the catalog is ALLWAYS (0,0) and the item positions need to be set separately for a given attitude. + + Parameters + ---------- + catalog : astropy.table.Table, optional + A star catalog. The following columns are used: idx, type, yang, zang, halfw. """ def __init__(self, catalog=None, parent=None): @@ -214,10 +244,11 @@ def set_pos_for_attitude(self, attitude): item.setPos(x, y) def boundingRect(self): + # this item draws nothing, it just holds children, but this is a required method. return QtC.QRectF(0, 0, 1, 1) def paint(self, _painter, _option, _widget): - # this item draws nothing, it just holds children + # this item draws nothing, it just holds children, but this is a required method. pass def __repr__(self): @@ -225,6 +256,18 @@ def __repr__(self): class Centroid(QtW.QGraphicsEllipseItem): + """ + QGraphicsItem representing a centroid. + + Centroids are depicted as blue circles with an X inside. + + Parameters + ---------- + imgnum : int + The image number (0-7). + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, imgnum, parent=None): self.imgnum = imgnum self.excluded = False @@ -258,6 +301,18 @@ def set_excluded(self, excluded): class FidLight(QtW.QGraphicsEllipseItem): + """ + QGraphicsItem representing a fiducial light. + + Fiducial lights are depicted as red circles with a cross inside. + + Parameters + ---------- + imgnum : int + The image number (0-7). + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, fid, parent=None): self.starcat_row = fid s = 27 @@ -275,6 +330,18 @@ def __init__(self, fid, parent=None): class StarcatLabel(QtW.QGraphicsTextItem): + """ + QGraphicsItem representing a label for a star in the star catalog. + + The label is the star's index in the catalog. + + Parameters + ---------- + star : astropy.table.Row + The proseco catalog row. + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, star, parent=None): self.starcat_row = star super().__init__(f"{star['idx']}", parent) @@ -290,6 +357,18 @@ def setPos(self, x, y): class GuideStar(QtW.QGraphicsEllipseItem): + """ + QGraphicsItem representing a guide star. + + Guide stars are depicted as green circles. + + Parameters + ---------- + star : astropy.table.Row + The proseco catalog row. + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, star, parent=None): self.starcat_row = star s = 27 @@ -299,6 +378,18 @@ def __init__(self, star, parent=None): self.setPen(QtG.QPen(QtG.QColor("green"), w)) class AcqStar(QtW.QGraphicsRectItem): + """ + QGraphicsItem representing an acquisition star. + + Acquisition stars are depicted as blue rectangles with width given by "halfw". + + Parameters + ---------- + star : astropy.table.Row + The proseco catalog row. + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, star, parent=None): self.starcat_row = star hw = star["halfw"] / 5 @@ -306,7 +397,20 @@ def __init__(self, star, parent=None): super().__init__(-hw, -hw, hw * 2, hw * 2, parent) self.setPen(QtG.QPen(QtG.QColor("blue"), w)) + class MonBox(QtW.QGraphicsRectItem): + """ + QGraphicsItem representing an monitoring star. + + Monitoring stars are depicted as orange rectangles with width given by "halfw". + + Parameters + ---------- + star : astropy.table.Row + The proseco catalog row. + parent : QGraphicsItem, optional + The parent item. + """ def __init__(self, star, parent=None): self.starcat_row = star # starcheck convention was to plot monitor boxes at 2X halfw @@ -317,6 +421,21 @@ def __init__(self, star, parent=None): class FieldOfView(QtW.QGraphicsItem): + """ + QGraphicsItem that groups together other items related to a (hypothetical) attitude. + + Items managed by this class: + + - CameraOutline: the outline of the ACA CCD. + - Centroids: the centroids of the stars. + + Parameters + ---------- + attitude : Quaternion, optional + The attitude of the camera associated with this FieldOfView. + alternate_outline : bool, optional + Boolean flag to use a simpler outline for the camera. + """ def __init__(self, attitude=None, alternate_outline=False): super().__init__() self.camera_outline = None @@ -343,9 +462,15 @@ def __init__(self, attitude=None, alternate_outline=False): self.setZValue(80) def get_attitude(self): + """ + Get the attitude of the camera associated with this FieldOfView. + """ return self._attitude def set_attitude(self, attitude): + """ + Set the attitude of the camera associated with this FieldOfView. + """ if hasattr(self, "_attitude") and attitude == self._attitude: return self._attitude = attitude @@ -361,6 +486,17 @@ def set_attitude(self, attitude): attitude = property(get_attitude, set_attitude) def set_pos_for_attitude(self, attitude): + """ + Set the position of all items in the field of view for the given attitude. + + This method is called when the attitude of the scene changes. The position of all items is + recalculated based on the new attitude. + + Parameters + ---------- + attitude : Quaternion + The attitude of the scene. + """ if self.camera_outline is not None: self.camera_outline.set_pos_for_attitude(attitude) self._set_centroid_pos_for_attitude(attitude) @@ -383,13 +519,22 @@ def _set_centroid_pos_for_attitude(self, attitude): centroid.setPos(x[i], y[i]) def boundingRect(self): + # this item draws nothing, it just holds children, but this is a required method. return QtC.QRectF(0, 0, 1, 1) def paint(self, _painter, _option, _widget): - # this item draws nothing, it just holds children + # this item draws nothing, it just holds children, but this is a required method. pass def set_centroids(self, centroids): + """ + Set the centroid values (usually from telemetry). + + Parameters + ---------- + centroids : astropy.table.Table + A table with the following columns: IMGNUM, AOACMAG, YAGS, ZAGS, IMGFID. + """ missing_cols = set(centroids.dtype.names) - set(self._centroids.dtype.names) if missing_cols: raise ValueError(f"Missing columns in centroids: {missing_cols}") @@ -440,6 +585,15 @@ def get_centroid_table(self): class CameraOutline(QtW.QGraphicsItem): + """ + A QGraphicsItem that represents the outline (the edges) of the ACA CCD. + + This is a graphics item to represent a hypothetical outline of the ACA CCD if the camera were + set to a given attitude. + + To calculate the position of the edges in the scene, the edges are first mapped to RA/dec, + and these RA/dec are later used to calculate the position in the scene coordinate system. + """ def __init__(self, attitude, parent=None, simple=False): super().__init__(parent) self.simple = simple @@ -544,7 +698,7 @@ def set_pos_for_attitude(self, attitude): def set_attitude(self, attitude): """ - Set the attitude of the camera corresponding to this frame. + Set the attitude of the camera corresponding to this outline. Note that this is not the attitude of the scene coordinate system. """ @@ -562,6 +716,9 @@ def set_attitude(self, attitude): ) def get_attitude(self): + """ + Get the attitude of the camera corresponding to this outline. + """ return self._attitude attitude = property(get_attitude, set_attitude) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 148746f..acfd9a1 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -718,7 +718,7 @@ def set_base_attitude(self, q): """ Sets the base attitude - The base attitude is the attitude corresponding to the origin of the scene. + The base attitude is the attitude of the scene. """ self.scene.set_attitude(q) From 11991ce45bcc21991298e0b70332b8629cc612a1 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 13:51:45 -0500 Subject: [PATCH 10/21] change centroid symbol and fixed FieldOfView.get_centroid_table --- aperoll/star_field_items.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 2b13254..0c94794 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -279,9 +279,10 @@ def __init__(self, imgnum, parent=None): pen = QtG.QPen(color, w) self.setPen(pen) - self._line_1 = QtW.QGraphicsLineItem(-s, 0, s, 0, self) + s /= np.sqrt(2) + self._line_1 = QtW.QGraphicsLineItem(-s, -s, s, s, self) self._line_1.setPen(pen) - self._line_2 = QtW.QGraphicsLineItem(0, -s, 0, s, self) + self._line_2 = QtW.QGraphicsLineItem(s, -s, -s, s, self) self._line_2.setPen(pen) self._label = QtW.QGraphicsTextItem(f"{imgnum}", self) @@ -535,7 +536,7 @@ def set_centroids(self, centroids): centroids : astropy.table.Table A table with the following columns: IMGNUM, AOACMAG, YAGS, ZAGS, IMGFID. """ - missing_cols = set(centroids.dtype.names) - set(self._centroids.dtype.names) + missing_cols = {"IMGNUM", "AOACMAG", "YAGS", "ZAGS", "IMGFID"} - set(centroids.dtype.names) if missing_cols: raise ValueError(f"Missing columns in centroids: {missing_cols}") @@ -569,17 +570,21 @@ def get_show_centroids(self): show_centroids = property(get_show_centroids, set_show_centroids) def get_centroid_table(self): - self._centroids["MAG_ACA"] = [ - (centroid.excluded or not centroid.isVisible()) - for centroid in self.centroids - ] + """ + Returns the centroid data as an astropy table, including whether each centroid is visible. + This is here for debugging purposes. + """ table = Table() table["IMGNUM"] = self._centroids["IMGNUM"] - table["MAG_ACA"] = self._centroids["MAG_ACA"] - table["excluded"] = self._centroids["MAG_ACA"] + table["AOACMAG"] = self._centroids["AOACMAG"] table["YAG"] = self._centroids["YAGS"] table["ZAG"] = self._centroids["ZAGS"] + table["IMGFID"] = self._centroids["IMGFID"] + table["excluded"] = self._centroids["excluded"] + table["visible"] = [ + centroid.isVisible() for centroid in self.centroids + ] return table From b9402f26f36ae24fcdd1e5e20d653552c8b6ba20 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 13:53:04 -0500 Subject: [PATCH 11/21] ruff --- aperoll/star_field_items.py | 34 ++++++++++++++++++++++------------ aperoll/widgets/star_plot.py | 10 ++++++++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 0c94794..60a6934 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -32,14 +32,14 @@ def star_field_position( - attitude=None, - yag=None, - zag=None, - ra=None, - dec=None, - row=None, - col=None, - ): + attitude=None, + yag=None, + zag=None, + ra=None, + dec=None, + row=None, + col=None, +): """ Calculate the position of an item in the star_field. @@ -118,6 +118,7 @@ class Star(QtW.QGraphicsEllipseItem): highlight : bool, optional If True, the star is highlighted in red. """ + def __init__(self, star, parent=None, highlight=False): s = symsize(star["MAG_ACA"]) rect = QtC.QRectF(-s / 2, -s / 2, s, s) @@ -268,6 +269,7 @@ class Centroid(QtW.QGraphicsEllipseItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, imgnum, parent=None): self.imgnum = imgnum self.excluded = False @@ -314,6 +316,7 @@ class FidLight(QtW.QGraphicsEllipseItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, fid, parent=None): self.starcat_row = fid s = 27 @@ -343,6 +346,7 @@ class StarcatLabel(QtW.QGraphicsTextItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, star, parent=None): self.starcat_row = star super().__init__(f"{star['idx']}", parent) @@ -370,6 +374,7 @@ class GuideStar(QtW.QGraphicsEllipseItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, star, parent=None): self.starcat_row = star s = 27 @@ -378,6 +383,7 @@ def __init__(self, star, parent=None): super().__init__(rect, parent) self.setPen(QtG.QPen(QtG.QColor("green"), w)) + class AcqStar(QtW.QGraphicsRectItem): """ QGraphicsItem representing an acquisition star. @@ -391,6 +397,7 @@ class AcqStar(QtW.QGraphicsRectItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, star, parent=None): self.starcat_row = star hw = star["halfw"] / 5 @@ -412,6 +419,7 @@ class MonBox(QtW.QGraphicsRectItem): parent : QGraphicsItem, optional The parent item. """ + def __init__(self, star, parent=None): self.starcat_row = star # starcheck convention was to plot monitor boxes at 2X halfw @@ -437,6 +445,7 @@ class FieldOfView(QtW.QGraphicsItem): alternate_outline : bool, optional Boolean flag to use a simpler outline for the camera. """ + def __init__(self, attitude=None, alternate_outline=False): super().__init__() self.camera_outline = None @@ -536,7 +545,9 @@ def set_centroids(self, centroids): centroids : astropy.table.Table A table with the following columns: IMGNUM, AOACMAG, YAGS, ZAGS, IMGFID. """ - missing_cols = {"IMGNUM", "AOACMAG", "YAGS", "ZAGS", "IMGFID"} - set(centroids.dtype.names) + missing_cols = {"IMGNUM", "AOACMAG", "YAGS", "ZAGS", "IMGFID"} - set( + centroids.dtype.names + ) if missing_cols: raise ValueError(f"Missing columns in centroids: {missing_cols}") @@ -582,9 +593,7 @@ def get_centroid_table(self): table["ZAG"] = self._centroids["ZAGS"] table["IMGFID"] = self._centroids["IMGFID"] table["excluded"] = self._centroids["excluded"] - table["visible"] = [ - centroid.isVisible() for centroid in self.centroids - ] + table["visible"] = [centroid.isVisible() for centroid in self.centroids] return table @@ -599,6 +608,7 @@ class CameraOutline(QtW.QGraphicsItem): To calculate the position of the edges in the scene, the edges are first mapped to RA/dec, and these RA/dec are later used to calculate the position in the scene coordinate system. """ + def __init__(self, attitude, parent=None, simple=False): super().__init__(parent) self.simple = simple diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index acfd9a1..5eb0b02 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -12,7 +12,13 @@ from Quaternion import Quat from aperoll import utils -from aperoll.star_field_items import Catalog, Centroid, FieldOfView, Star, star_field_position +from aperoll.star_field_items import ( + Catalog, + Centroid, + FieldOfView, + Star, + star_field_position, +) @dataclass @@ -576,7 +582,7 @@ def set_star_positions(self): x, y = star_field_position( self._attitude, ra=self._stars["RA_PMCORR"], - dec=self._stars["DEC_PMCORR"] + dec=self._stars["DEC_PMCORR"], ) for idx, item in enumerate(self._stars): item["graphic_item"].setPos(x[idx], y[idx]) From 5ef2e9a1960a563f982f9a51d2ce34a015934846 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Fri, 22 Nov 2024 13:56:40 -0500 Subject: [PATCH 12/21] ruff --- aperoll/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aperoll/utils.py b/aperoll/utils.py index af16491..bb9e870 100644 --- a/aperoll/utils.py +++ b/aperoll/utils.py @@ -62,9 +62,9 @@ def get_camera_fov_frame(): "col": cross_1[1], } - for key in frame: - frame[key]["yag"], frame[key]["zag"] = pixels_to_yagzag( - frame[key]["row"], frame[key]["col"], allow_bad=True + for value in frame.values(): + value["yag"], value["zag"] = pixels_to_yagzag( + value["row"], value["col"], allow_bad=True ) return frame From e1439f372016fa46ba0c2bf3459e602a9d307d92 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 26 Nov 2024 11:58:59 -0500 Subject: [PATCH 13/21] remove menu bar and add quick config to context menu --- aperoll/widgets/star_plot.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 5eb0b02..1f2caaa 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -69,7 +69,7 @@ class StarFieldState: { # use case: "alternate" real-time telemetry # (attitude set to user attitude, but showing the camera outline in the alternate FOV) - "name": "Alternate", + "name": "Alternate FOV", "onboard_attitude_slot": "alternate_attitude", "enable_fov": True, "enable_alternate_fov": True, @@ -99,21 +99,6 @@ def __init__(self, scene=None): self._draw_frame = False - application = QtW.QApplication.instance() - main_windows = [ - w for w in application.topLevelWidgets() if isinstance(w, QtW.QMainWindow) - ] - for window in main_windows: - menu_bar = window.menuBar() - menu = menu_bar.addMenu("Aperoll") - menu = menu.addMenu("Star Field Quick Config") - for state in self.scene().states.values(): - action = QtW.QAction(state.name, self) - action.triggered.connect( - lambda _checked, name=state.name: self.scene().set_state(name) - ) - menu.addAction(action) - def _get_draw_frame(self): return self._draw_frame @@ -304,6 +289,12 @@ def contextMenuEvent(self, event): # noqa: PLR0912, PLR0915 reset_fov_action = QtW.QAction("Reset Alt FOV", menu, checkable=False) menu.addAction(reset_fov_action) + config_menu = menu.addMenu("Quick Config") + + for state in self.scene().states.values(): + action = QtW.QAction(state.name, config_menu) + config_menu.addAction(action) + result = menu.exec_(event.globalPos()) if result is not None: if centroid is not None and result.text().startswith("include slot"): @@ -333,6 +324,8 @@ def contextMenuEvent(self, event): # noqa: PLR0912, PLR0915 self.scene().state.auto_proseco = result.isChecked() if result.isChecked(): self.update_proseco.emit() + elif result.text() in self.scene().states: + self.scene().set_state(result.text()) else: print(f"Action {result.text()} not implemented") event.accept() From 40f6aa4aaf4aff824ad3a4a2d91fef0027afc695 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 26 Nov 2024 12:00:24 -0500 Subject: [PATCH 14/21] change default centroid values so they are not shown --- aperoll/star_field_items.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 60a6934..df38bea 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -453,7 +453,8 @@ def __init__(self, attitude=None, alternate_outline=False): self.attitude = attitude self.centroids = [Centroid(i, parent=self) for i in range(8)] self._centroids = np.array( - [(0, 511, 511, 511, 511, 14, 0, 0, 0, 0) for _ in self.centroids], + # pixels right outside the CCD by default + [(0, 511, 511, 511, 511, 14, -2490, 2507, False, False) for _ in self.centroids], dtype=[ ("IMGNUM", int), ("IMGROW0_8X8", float), From 62b57524caf5a53ae877ed1d0386b5ec316f5b45 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 26 Nov 2024 12:00:55 -0500 Subject: [PATCH 15/21] make sure state values are copied when resetting --- aperoll/widgets/star_plot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aperoll/widgets/star_plot.py b/aperoll/widgets/star_plot.py index 1f2caaa..3045671 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, replace import agasc import numpy as np @@ -442,13 +442,15 @@ def get_state(self): return self._state def set_state(self, state_name): - self._state = self.states[state_name] + # copy self.states[state_name] so self.states[state_name] is not modified + self._state = replace(self.states[state_name]) self.enable_fov(self._state.enable_fov) self.enable_alternate_fov(self._state.enable_alternate_fov) self.enable_catalog(self._state.enable_catalog) self.alternate_fov.show_centroids = self._state.enable_alternate_fov_centroids self.main_fov.show_centroids = self._state.enable_centroids + self.onboard_attitude_slot = self._state.onboard_attitude_slot self.state_changed.emit(state_name) From 341ceb852f0a56b8433f99d767ffb8247031a3fc Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 26 Nov 2024 12:04:11 -0500 Subject: [PATCH 16/21] ruff --- aperoll/star_field_items.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index df38bea..46c7e11 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -454,7 +454,10 @@ def __init__(self, attitude=None, alternate_outline=False): self.centroids = [Centroid(i, parent=self) for i in range(8)] self._centroids = np.array( # pixels right outside the CCD by default - [(0, 511, 511, 511, 511, 14, -2490, 2507, False, False) for _ in self.centroids], + [ + (0, 511, 511, 511, 511, 14, -2490, 2507, False, False) + for _ in self.centroids + ], dtype=[ ("IMGNUM", int), ("IMGROW0_8X8", float), From d8b0319bcf96a645b2164108fceb5b338e0d6879 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 26 Nov 2024 14:18:01 -0500 Subject: [PATCH 17/21] fixup when setting catalog in StarPlot (check dtype too) --- 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 3045671..c8f6cc6 100644 --- a/aperoll/widgets/star_plot.py +++ b/aperoll/widgets/star_plot.py @@ -740,6 +740,7 @@ def set_catalog(self, catalog): if ( self._catalog is not None and (len(self._catalog) == len(catalog)) + and (self._catalog.dtype == catalog.dtype) and np.all(self._catalog == catalog) ): return From 20a834dd3d45c9bc3137259fcb5b58d2a9557623 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 12 Dec 2024 15:05:47 -0500 Subject: [PATCH 18/21] show fid centroids after changing color --- aperoll/star_field_items.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 46c7e11..6f4881e 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -302,6 +302,14 @@ def set_excluded(self, excluded): self._line_1.setPen(pen) self._line_2.setPen(pen) + def set_fiducial(self, fiducial): + color = QtG.QColor("red") if fiducial else QtG.QColor("blue") + pen = self.pen() + pen.setColor(color) + self.setPen(pen) + self._line_1.setPen(pen) + self._line_2.setPen(pen) + class FidLight(QtW.QGraphicsEllipseItem): """ @@ -571,9 +579,9 @@ def set_show_centroids(self, show=True): | (row > 511) | (col < -511) | (col > 511) - | (self._centroids["IMGFID"]) ) for i, centroid in enumerate(self.centroids): + centroid.set_fiducial(self._centroids["IMGFID"][i]) if off_ccd[i]: centroid.setVisible(False) else: From 6ab98f9633975042f2fb480eb6c7f4011fe0194f Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Thu, 12 Dec 2024 15:12:32 -0500 Subject: [PATCH 19/21] fixup --- aperoll/star_field_items.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 6f4881e..38e996a 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -301,6 +301,7 @@ def set_excluded(self, excluded): self.setPen(pen) self._line_1.setPen(pen) self._line_2.setPen(pen) + self._label.setDefaultTextColor(color) def set_fiducial(self, fiducial): color = QtG.QColor("red") if fiducial else QtG.QColor("blue") @@ -309,6 +310,7 @@ def set_fiducial(self, fiducial): self.setPen(pen) self._line_1.setPen(pen) self._line_2.setPen(pen) + self._label.setDefaultTextColor(color) class FidLight(QtW.QGraphicsEllipseItem): From bcc262da9c14c16a122f1bae511c81950a6b8449 Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 28 Jan 2025 15:08:25 -0500 Subject: [PATCH 20/21] ruff --- aperoll/star_field_items.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index 38e996a..a3180f2 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -157,12 +157,12 @@ 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"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"pos err: {self.star['POS_ERR'] / 1000} mas\n"
             f"VAR:     {self.star['VAR']}"
             "
" ) @@ -576,12 +576,7 @@ def set_show_centroids(self, show=True): row, col = yagzag_to_pixels( self._centroids["YAGS"], self._centroids["ZAGS"], allow_bad=True ) - off_ccd = ( - (row < -511) - | (row > 511) - | (col < -511) - | (col > 511) - ) + off_ccd = (row < -511) | (row > 511) | (col < -511) | (col > 511) for i, centroid in enumerate(self.centroids): centroid.set_fiducial(self._centroids["IMGFID"][i]) if off_ccd[i]: From c2e8b8c6c5fa1d42db38ca18079c7501d9deec9a Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Wed, 18 Dec 2024 10:14:05 -0500 Subject: [PATCH 21/21] added .fiducial attribute to Centroid item and improved the way style is set --- aperoll/star_field_items.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/aperoll/star_field_items.py b/aperoll/star_field_items.py index a3180f2..5ae2bbf 100644 --- a/aperoll/star_field_items.py +++ b/aperoll/star_field_items.py @@ -270,7 +270,7 @@ class Centroid(QtW.QGraphicsEllipseItem): The parent item. """ - def __init__(self, imgnum, parent=None): + def __init__(self, imgnum, parent=None, fiducial=False): self.imgnum = imgnum self.excluded = False s = 18 @@ -283,34 +283,32 @@ def __init__(self, imgnum, parent=None): s /= np.sqrt(2) self._line_1 = QtW.QGraphicsLineItem(-s, -s, s, s, self) - self._line_1.setPen(pen) self._line_2 = QtW.QGraphicsLineItem(s, -s, -s, s, self) - self._line_2.setPen(pen) self._label = QtW.QGraphicsTextItem(f"{imgnum}", self) self._label.setFont(QtG.QFont("Arial", 30)) - self._label.setDefaultTextColor(color) self._label.setPos(30, -30) - def set_excluded(self, excluded): - self.excluded = excluded + self.fiducial = fiducial + self._set_style() + + def _set_style(self): + color = QtG.QColor("red") if self.fiducial else QtG.QColor("blue") + color.setAlpha(85 if self.excluded else 255) pen = self.pen() - color = pen.color() - color.setAlpha(85 if excluded else 255) pen.setColor(color) self.setPen(pen) self._line_1.setPen(pen) self._line_2.setPen(pen) self._label.setDefaultTextColor(color) + def set_excluded(self, excluded): + self.excluded = excluded + self._set_style() + def set_fiducial(self, fiducial): - color = QtG.QColor("red") if fiducial else QtG.QColor("blue") - pen = self.pen() - pen.setColor(color) - self.setPen(pen) - self._line_1.setPen(pen) - self._line_2.setPen(pen) - self._label.setDefaultTextColor(color) + self.fiducial = fiducial + self._set_style() class FidLight(QtW.QGraphicsEllipseItem):