Source code for lfd.results.event

"""Event contains a single linear feature detected with all its measured
parameters on a single Frame. Event is intended to be used as the basic object
on which work is done, as it encompases all information (times, frame,
points...) required. While that may be true, Event is still composed of
multiple smaller movimg pieces with which it can have complex relationships
with so there are some things worth remembering when working with Event.
"""
import sqlalchemy as sql
from sqlalchemy.orm import relationship, composite
from sqlalchemy.ext.hybrid import hybrid_property

from astropy.time import Time

from lfd.results import Base
from lfd.results import __query_aliases as query_aliases
from lfd.results.point import Point
from lfd.results.basictime import BasicTime, LineTime
from lfd.results.utils import session_scope

__all__ = ["Event"]


[docs]class Event(Base): """Class Event maps table 'events'. Corresponds to a single linear feature detection. Contains the measured properties of the feature and links to the Frame on which it was detected. Parameters ---------- id : int PrimaryKey, autoincremental _run : int run id (ForeignKey) _camcol : int camcol id (ForeignKey) _filter : str filter id (ForeignKey) _field : int field id (ForeignKey) frame : sql.relationshitp Frame on which the linear feature was detected, a many to one relationship to frames x1 : float x frame coordinate of point 1 of the linear feature y1 : float y frame coordinate of point 1 of the linear feature x2 : float x frame coordinate of point 2 of the linear feature y2 : float y frame coordinate of point 2 of the linear feature cx1 : float x ccd coordinate of point 1 of the linear feature cy2 : float x ccd coordinate of point 1 of the linear feature cx2 : float x ccd coordinate of point 2 of the linear feature cy2 : y ccd coordinate of point 2 of the linear feature p1 : see class Point composite Column mapping of x1, y1 to a point p1 p2 : see class Point,composite Column mapping of x2, y2 to a point p2 start_t : if possible, the time of the first detection of the linear feature on the frame, see class BasicTime end_t : if possible, the time of the last detection of the linear feature on the frame, see class BasicTime lt : not very usefull, see class LineTime Examples -------- A Frame object reference is required. See lfd.results.frame.Frame for documentation. At minimum, supply the Frame object and coordinates of two points defining a line: >>> foo = Event(Frame, 1, 1, 2, 2) By default the coordinates are considered to be in "frame" coordinate sys. Optionally specify the "ccd" as the reference coordinate system: >>> foo = Event(Frame, 1, 1, 2, 2, coordsys="ccd") Optionaly all values could be supplied: >>> foo = Event(Frame, 1, 1, 2, 2, 1, 1, 2, 2) or on an example of a different CCD (4, 'i'): >>> foo = Event(f, 1, 1, 1, 1, 11377, 27010, 11377, 2710) or verbosely: >>> foo = Event(frame=Frame, x1=1, y1=1, x2=2, y2=2, cx1=1, cy1=1, cx2=2, cy2=2, coordsys="frame") .. caution:: When specifying coordinates in the ccd coordinate system be mindful of the fact that coordinates (0, 0) in 'ccd' frame are upper left corner of the ccd array (camcol 1, filter 'r'). It is not possible to have CCD coordinates (1, 1) or (2, 2) except on the first chip of the CCD-array. If the Frame reference is with respect to some other camcol and filter the ccd coordinates must, otherwise an Error will be raised. It is possible to submit start or end time of the linear feature in any the following formats: Astropy Time object, float number in mjd format, float number in sdss-tai format: Astropy Time Object - supply an instantiated Astropy Time Object: >>> st = astropy.time.Time(58539.0, format="mjd") >>> et = astropy.time.Time("2019-02-25 00:00:10.000") >>> foo = Event(frame, 1, 1, 2, 2, start_t=st, end_t=et) Any Astropy Time supported format such as mjd, iso, isot, jd etc... >>> foo = Event(frame, 1, 1, 2, 2, end_t=63072064.184, format="cxcsec") or in the SDSS modified tai format: >>> foo = Event(frame, 1, 1, 2, 2, start_t=4575925956.49, format="sdss-tai") """ __tablename__ = "events" id = sql.Column(sql.Integer, primary_key=True) # a copy of each of the foreign key components must be kept for join to work _run = sql.Column(sql.Integer) _camcol = sql.Column(sql.Integer) _filter = sql.Column(sql.String(length=1)) _field = sql.Column(sql.Integer) # we hide the coordinates because they should be accessed through properties _x1 = sql.Column(sql.Float, nullable=False) _y1 = sql.Column(sql.Float, nullable=False) _x2 = sql.Column(sql.Float, nullable=False) _y2 = sql.Column(sql.Float, nullable=False) _cx1 = sql.Column(sql.Float, nullable=False) _cy1 = sql.Column(sql.Float, nullable=False) _cx2 = sql.Column(sql.Float, nullable=False) _cy2 = sql.Column(sql.Float, nullable=False) verified = sql.Column(sql.Boolean, nullable=False, default=False) false_positive = sql.Column(sql.Boolean, nullable=False, default=True) start_t = sql.Column(BasicTime) end_t = sql.Column(BasicTime) # the order in instantiation of the composite and the Point class in __init__ # itself is **very important**. The Point will resolve its init procedures # itself so that various different inits are possible, but this is based on # the order of init parameters and on their value (Truthy or None). If the # order of the init parameters in the __init__ function does not match the # order of parameters in the composite, when instantiating from the DB wrong # values will be sent as wrong parameters. Additionally _camcol and _filter # can not be in front of the _cx and _cy because __composite_values__ of # Point DO NOT CHANGE THEM. Therefore the x, y, cx, cy will map to # x, y, camcol, filter, cx, cy but because types won't be correct - it will # default to None lt = composite(LineTime, start_t, end_t) p1 = composite(Point, _x1, _y1, _cx1, _cy1, _camcol, _filter) p2 = composite(Point, _x2, _y2, _cx2, _cy2, _camcol, _filter) #http://docs.sqlalchemy.org/en/latest/orm/relationship_persistence.html#mutable-primary-keys-update-cascades # THIS IS THE ONLY WAY to make sure foreign keys work as foreign composite # key. It is a many-to-one relationship where 1 frame can have many events. # On-update cascades shoud ensure that an update of frames row all events # rows are updated; and not orphaned; but only works on commit. __table_args__ = ( sql.ForeignKeyConstraint(['_run', '_camcol', "_filter", "_field"], ['frames.run', 'frames.camcol', "frames.filter", "frames.field"], onupdate="CASCADE"),{} ) # back_populates will create an attribute on Event that will make the Frame # object accessible through Event. How do cascades work, or why don't they # work?!?! frame = relationship("Frame", back_populates="events", cascade="save-update,merge,expunge") def __init__(self, frame, x1=None, y1=None, x2=None, y2=None, cx1=None, cy1=None, cx2=None, cy2=None, start_t=None, end_t=None, coordsys="frame", **kwargs): """foo = Event(frame=Frame, x1=1, y1=1, x2=2, y2=2, cx1=1, cy1=1, cx2=2, cy2=2, start_t=58539.0, end_t=58541.0 format="mjd", coordsys="frame") Frame object reference is required alongside with 4 coordinates that define two points P1 and P2 that lie on the linear feature. They can be in either the "frame" or "ccd" coordinate system. foo = Event(Frame, 1, 1, 2, 2) foo = Event(Frame, 1, 1, 2, 2, coordsys="ccd") foo = Event(Frame, 1, 1, 2, 2, 1, 1, 2, 2) foo = Event(f, 1, 1, 1, 1, 11376.462868, 2709.4435401, 11376.462868, 2709.4435401, 58539.0, 58541.0, format="mjd", coordsys="frame") See class docstring for more details. """ # we check that the points are actually sensible and not inconsistent p1 = Point(x=x1, y=y1, cx=cx1, cy=cy1, camcol=frame.camcol, filter=frame.filter, coordsys=coordsys) p2 = Point(x=x2, y=y2, cx=cx2, cy=cy2, camcol=frame.camcol, filter=frame.filter, coordsys=coordsys) if any([frame.filter != p1._filter, frame.filter != p2._filter, frame.camcol != p1._camcol, frame.camcol != p2._camcol]): msg = ("Instantiation inconsistency: Supplied coordinates do not " "match the given Frame. Expected camcol={0}, filter={1} but " "calculated P1(camcol={2}, filter={3}) and " "P2(camcol={4}, filter={5})") msg = msg.format(frame.camcol, frame.filter, p1.camcol, p1.filter, p2.camcol, p2.filter) raise sql.exc.DataError(msg) # if they are we use them, because of Mutable Composite this updates # the relevant fields in the object immediatelly self.p1 = p1 self.p2 = p2 # the same is not true for frame - it's a relationship and won't fill in # object attributes untill commited to DB. So, we fill then in manually self.frame = frame self._run = frame.run self._camcol = frame.camcol self._filter = frame.filter self._field = frame.field # assume sdss-tai format in case format is not supplied. t_format = kwargs.pop("format", "sdss-tai") self.start_t = self.__init_t(start_t, t_format) self.end_t = self.__init_t(end_t, t_format) def __init_t(self, t, format): # It might not be possible to determine the times of the linear # feature's first and last recording so we return None when that is the # case. Otherwise we return the Astropy's Time object. This discrepancy # does not exist for Frame because t can not be None in that case. if t is None: return None if isinstance(t, Time): self.t = t elif format == "sdss-tai": self.t = Time(t/(24*3600.), format="mjd") else: self.t = Time(t, format=format) def __repr__(self): # returns a string in following format: # <library.package.module.ClassName(all_class_components_and_origins)> m = self.__class__.__module__ n = self.__class__.__name__ f = repr(self.frame).split("results.")[-1][:-1] p1 = repr(self.p1).split("results.")[-1][:-1] p2 = repr(self.p2).split("results.")[-1][:-1] st = self.start_t.iso if self.start_t is not None else None et = self.end_t.iso if self.end_t is not None else None return "<{0}.{1}({4}, {2}, {3}, {5})>".format(m, n, p1, p2, f, st, et) def __str__(self): # returns a string in the following format: # Event(Frame, Point, Point, BasicTime, BasicTime) p1 = str(self.p1) p2 = str(self.p2) st = str(self.start_t) et = str(self.end_t) frame = str(self.frame) printstr = "Event({0}, {1}, {2}, {3}, {4})" return printstr.format(frame, p1, p2, st, et)
[docs] def _findPointsOnSides(self, m, b): """Looking for an intersection of a horizontal/vertical borders with a line equation does not neccessarily return a point within the range we're looking for. It is easier and faster to do it manually. Each individual border will be checked manually and if it satisfies, two coordinates (defining a Point) will be appended to a list. Special cases of (0, 0) and (2048, 2048) will satisfy both border conditions and so will be duplicated in the result. Interestingly, if working from 'frame' reference system it's not neccessary to know which reference frame we're looking at. Parameters ---------- m : float line slope b : float line y intercept """ # make new coords newx = [] newy = [] success = False # check vertical border x=0, then y=b, ignore if it's outside CCD if b >= 0 and b <= H_FILTER: newx.append(0) newy.append(b) # check the vertical border x=W_CAMCOL, then y = m*W_CAMCOL+b tmp = m*W_CAMCOL + b if tmp >= 0 and tmp <= W_CAMCOL: newx.append(W_CAMCOL) newy.append(tmp) # check the horizontal border y=0, then x = -b/m tmp = -b/m if tmp >= 0 and tmp <= W_CAMCOL: newx.append(tmp) newy.append(0) # check the horizontal border y=H_FILTER, then x = (H_FILTER-b)/m tmp = (H_FILTER-b)/m if tmp >= 0 and tmp <= W_CAMCOL: newx.append(tmp) newy.append(H_FILTER) return newx, newy
[docs] def snap2ccd(self): """Snap the curent coordinates to the points of intersection of the reference frame CCD border and the linear feature. A negatively sloped 45° linear feature passes diagonally across the first CCD in the array (1, 'r'), cutting through both its corners. Such feature could be defined by P1(-1000, -1000) and P2(10000, 10000). Snap will determine the two border points P1(0,0) and P2(2048, 2048). """ # calculate the line slope and intercept y=mx+b, pray it works, needs # checks for verticality and horizontality m = (self.y2-self.y1)/(self.x2-self.x1) b = -m*self.x1 + self.y1 newx, newy = self._findPointsOnSides(m, b) # The pints (0,0) and (2048, 2048) are special cases since they belong # to both borders so returned results are duplicated. if (len(newx) == 2 and len(newy) == 2) or \ (len(newx) == 4 and len(newy) == 4): self.x1 = newx[0] self.x2 = newx[1] self.y1 = newy[0] self.y2 = newy[1] else: msg = "Could not compute edge points, returned: P1{0} and P2{1}." raise ValueError(msg.format(newx, newy))
[docs] @classmethod def query(cls, condition=None): """A class method that can be used to query the Event table. Appropriate for interactive work, not as appropriate for large codebase usage. See package help for more details on how the Session is kept open. Will return a query object, not the query result. If condition is supplied it is interpreted as a common string SQL. It's sufficient to use the names of mapped classes and their attributes as they will automatically be replaced by the correct table and column names. Examples: Event.query().all() Event.query().first() Event.query("run == 3").all() Event.query("Event.run > 3").all() Event.query("Frame.t > 4412911072y").all() Event.query("events.cx1 > 10000").all() Event.query("frames.filter == 'i'").all() """ if condition is None: with session_scope() as s: return s.query(cls).join("frame") # the condition is essentially consistent of two for key, val in query_aliases.items(): if key[:1] in (["x", "y", "c"]): condition = condition.replace("."+key, "."+val) else: condition = condition.replace(key, val) with session_scope() as s: return s.query(cls).join("frame").filter(sql.text(condition))
########################################################################### ################## frame ############# ########################################################################### @hybrid_property def run(self): return self._run @hybrid_property def camcol(self): return self._camcol @hybrid_property def filter(self): return self._filter @hybrid_property def field(self): return self._field ########################################################################### ################### p1 ############## ########################################################################### def __check_sensibility(self, attr): if attr[-1:] == "1": camcol, filter = self.p1._camcol, self.p1._filter elif attr[-1:] == "2": camcol, filter = self.p2._camcol, self.p2._filter if camcol != self._camcol or filter != self._filter: msg = ("New camcol and filter ({0}, {1}) do not correspond to Frame " "camcol and filter ({2}, {3}) anymore. If commited to DB it " "will not be recoverable.") msg = msg.format(camcol, filter, self._camcol, self._filter) warnings.warn(msg, SyntaxWarning) @hybrid_property def x1(self): return self._x1 @x1.setter def x1(self, val): self.p1._initFrame(val, self.y1, self.camcol, self.filter) @hybrid_property def y1(self): return self._y1 @y1.setter def y1(self, val): self.p1._initFrame(self.x1, val, self.camcol, self.filter) @hybrid_property def cx1(self): return self._cx1 @cx1.setter def cx1(self, val): self.p1._initCCD(val, self.cy1) self.__check_sensibility("cx1") @hybrid_property def cy1(self): return self._cy1 @cy1.setter def cy1(self, val): self.p1._initCCD(self.cx1, val) self.__check_sensibility("cy1") ########################################################################### ################### p2 ############## ########################################################################### @hybrid_property def x2(self): return self._x2 @x2.setter def x2(self, val): self.p2._initFrame(val, self.y2, self.camcol, self.filter) @hybrid_property def y2(self): return self._y2 @y2.setter def y2(self, val): self.p2._initFrame(self.x2, val, self.camcol, self.filter) @hybrid_property def cx2(self): return self._cx2 @cx2.setter def cx2(self, val): self.p2._initCCD(val, self.cy2) self.__check_sensibility("cx2") @hybrid_property def cy2(self): return self._cy2 @cy2.setter def cy2(self, val): self.p2._initCCD(self.cx2, val) self.__check_sensibility("cy2")