Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actively transform the scene when the view is dragged #4

Merged
merged 10 commits into from
Nov 21, 2024
220 changes: 220 additions & 0 deletions aperoll/star_field_items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"""
Collection of QGraphicsItem subclasses to represent star field items in the star field view.
"""

import numpy as np
from astropy.table import Table
from chandra_aca.transform import (
radec_to_yagzag,
yagzag_to_pixels,
yagzag_to_radec,
)
from proseco.acq import get_acq_candidates_mask
from PyQt5 import QtCore as QtC
from PyQt5 import QtGui as QtG
from PyQt5 import QtWidgets as QtW

__all__ = [
"Star",
"Catalog",
"FidLight",
"StarcatLabel",
"GuideStar",
"AcqStar",
"MonBox",
]


def symsize(mag):
# map mags to figsizes, defining
# mag 6 as 40 and mag 11 as 3
# interp should leave it at the bounding value outside
# the range
return np.interp(mag, [6.0, 11.0], [32.0, 8.0])


class Star(QtW.QGraphicsEllipseItem):
def __init__(self, star, parent=None, highlight=False):
s = symsize(star["MAG_ACA"])
rect = QtC.QRectF(-s / 2, -s / 2, s, s)
super().__init__(rect, parent)
# self._stars = Table([star], names=star.colnames, dtype=star.dtype)
self.star = star
self.highlight = highlight
color = self.color()
self.setBrush(QtG.QBrush(color))
self.setPen(QtG.QPen(color))
self.included = {
"acq": None,
"guide": None,
}
# stars are stacked in z by magnitude, so small stars never hide behind big ones
# the brightest entry in the catalog has MAG_ACA = -1.801
# the faintest entry in the catalog has MAG_ACA ~ 21.5
self.setZValue(20 + star["MAG_ACA"])

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):
return not get_acq_candidates_mask(self.star)

def text(self):
return (
"<pre>"
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']}"
"</pre>"
)


class Catalog(QtW.QGraphicsItem):
"""
Utility class to keep together all graphics item for a star catalog.

Note that the position of the catalog is ALLWAYS (0,0) and the item positions need to be set

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in docstring.

separately for a given attitude.
"""

def __init__(self, catalog, parent=None):
super().__init__(parent)
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)
# so these items move with the corresponding star.
cat["ra"], cat["dec"] = yagzag_to_radec(
cat["yang"], cat["zang"], self.starcat.att
)
gui_stars = cat[(cat["type"] == "GUI") | (cat["type"] == "BOT")]
acq_stars = cat[(cat["type"] == "ACQ") | (cat["type"] == "BOT")]
fids = cat[cat["type"] == "FID"]
mon_wins = cat[cat["type"] == "MON"]

self.star_labels = [StarcatLabel(star, self) for star in cat]
self.guide_stars = [GuideStar(gui_star, self) for gui_star in gui_stars]
self.acq_stars = [AcqStar(acq_star, self) for acq_star in acq_stars]
self.mon_stars = [MonBox(mon_box, self) for mon_box in mon_wins]
self.fid_lights = [FidLight(fid, self) for fid in fids]

def setPos(self, *_args, **_kwargs):
# the position of the catalog is ALLWAYS (0,0)
pass

def set_pos_for_attitude(self, attitude):
"""
Set the position of all items in the catalog for a given attitude.

Calling QGraphicsItem.set_pos would not work. Children positions are relative to the
parent, but in reality the relative distances between items changes with the attitude.
One cannot change the position of a single item and then get the rest as a relative shift.
Each item needs to be set individually.
"""
# 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
)
row, col = yagzag_to_pixels(yag, zag, allow_bad=True)
# item.setPos(-yag, -zag)
item.setPos(row, -col)

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 __repr__(self):
return repr(self.starcat)


class FidLight(QtW.QGraphicsEllipseItem):
def __init__(self, fid, parent=None):
self.starcat_row = fid
s = 27
w = 3
rect = QtC.QRectF(-s, -s, 2 * s, 2 * s)
super().__init__(rect, parent)
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)
line = QtW.QGraphicsLineItem(0, -s, 0, s, self)
line.setPen(pen)


class StarcatLabel(QtW.QGraphicsTextItem):
def __init__(self, star, parent=None):
self.starcat_row = star
super().__init__(f"{star['idx']}", parent)
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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if everything basically gets plugged in as (row, -col) - would there be value in flipping the function signature and adding a docstring?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPos is not written by me. It is the Qt function, and I don't want to start overriding all setPos functions for all possible items, and we do want to keep the appearance, so I do not see a way other than having minus signs all over.

There might be some kind of hack, but I prefer to keep it explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean setPos for all other QGraphicsItems. This one is overridden but it is an exception.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okey Doke.

rect = self.boundingRect()
super().setPos(
x + self._offset - rect.width() / 2, y - self._offset - rect.height() / 2
)


class GuideStar(QtW.QGraphicsEllipseItem):
def __init__(self, star, parent=None):
self.starcat_row = star
s = 27
w = 5
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"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably go through and remove commented out code in next PR.

self.setPos(star["row"], -star["col"])


class AcqStar(QtW.QGraphicsRectItem):
def __init__(self, star, parent=None):
self.starcat_row = star
hw = star["halfw"] / 5
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):
self.starcat_row = star
# starcheck convention was to plot monitor boxes at 2X halfw
hw = star["halfw"] / 5
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"])
60 changes: 60 additions & 0 deletions aperoll/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,67 @@
import numpy as np
from chandra_aca.transform import pixels_to_yagzag, yagzag_to_pixels
from ska_helpers import logging

logger = logging.basic_logger("aperoll")


# The nominal origin of the CCD, in pixel coordinates (yagzag_to_pixels(0, 0))
CCD_ORIGIN = yagzag_to_pixels(
0, 0
) # (6.08840495576943, 4.92618563916467) as of this writing
# The (0,0) point of the CCD coordinates in (yag, zag)
YZ_ORIGIN = pixels_to_yagzag(0, 0)


class AperollException(RuntimeError):
pass


def get_camera_fov_frame():
"""
Paths that correspond ot the edges of the ACA CCD and the quadrant boundaries.
"""
frame = {}
N = 100
edge_1 = np.array(
[[-520, i] for i in np.linspace(-512, 512, N)]
+ [[i, 512] for i in np.linspace(-520, 520, N)]
+ [[520, i] for i in np.linspace(512, -512, N)]
+ [[i, -512] for i in np.linspace(520, -520, N)]
+ [[-520, 0]]
).T
frame["edge_1"] = {
"row": edge_1[0],
"col": edge_1[1],
}

edge_2 = np.array(
[[-512, i] for i in np.linspace(-512, 512, N)]
+ [[i, 512] for i in np.linspace(-512, 512, N)]
+ [[512, i] for i in np.linspace(512, -512, N)]
+ [[i, -512] for i in np.linspace(512, -512, N)]
+ [[-512, 0]]
).T
frame["edge_2"] = {
"row": edge_2[0],
"col": edge_2[1],
}

cross_2 = np.array([[i, 0] for i in np.linspace(-511, 511, N)]).T
frame["cross_2"] = {
"row": cross_2[0],
"col": cross_2[1],
}

cross_1 = np.array([[0, i] for i in np.linspace(-511, 511, N)]).T
frame["cross_1"] = {
"row": cross_1[0],
"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
)

return frame
12 changes: 7 additions & 5 deletions aperoll/widgets/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def __init__(self, opts=None): # noqa: PLR0915
sparkles.core.check_catalog(aca)

if starcat is not None:
self.plot.set_catalog(starcat, update=False)
self.plot.set_catalog(starcat)
self.starcat_view.set_catalog(aca)

def closeEvent(self, event):
Expand All @@ -189,7 +189,9 @@ def closeEvent(self, event):
event.accept()

def _parameters_changed(self):
self._data.reset(self.parameters.proseco_args())
proseco_args = self.parameters.proseco_args()
self.plot.set_base_attitude(proseco_args["att"])
self._data.reset(proseco_args)

def _init(self):
if self.parameters.values:
Expand All @@ -199,8 +201,8 @@ def _init(self):
aca_attitude = Quat(
equatorial=(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)
self.plot.set_base_attitude(aca_attitude)
self.plot.set_time(time)

def _reset(self):
self.parameters.set_parameters(**self.opts)
Expand All @@ -226,7 +228,7 @@ def _run_proseco(self):
"""
if self._data.proseco:
self.starcat_view.set_catalog(self._data.proseco["aca"])
self.plot.set_catalog(self._data.proseco["catalog"], update=False)
self.plot.set_catalog(self._data.proseco["catalog"])

def _export_proseco(self):
"""
Expand Down
1 change: 1 addition & 0 deletions aperoll/widgets/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def proseco_args(self):
"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
"dyn_bgd_n_faint": 2,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should not be needed now that we've changed the proseco default.

}

for key in [
Expand Down
Loading