This repository has been archived by the owner on Oct 26, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
database.py
1834 lines (1492 loc) · 71.2 KB
/
database.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#! /usr/bin/env python2
# Copyright (c) 2012 Victor Terron. All rights reserved.
# Institute of Astrophysics of Andalusia, IAA-CSIC
#
# This file is part of LEMON.
#
# LEMON is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
"""
This module implements LEMONdB, the interface to the SQLite databases to which
photometric information and light curves are saved. These databases contain all
the information that may be needed for the data analysis.
"""
import collections
import copy
import itertools
import math
import numpy
import numbers
import operator
import os
import random
import string
import sqlite3
import tempfile
# LEMON modules
import astromatic
import json_parse
import passband
import util
class DBStar(object):
"""Encapsulates the instrumental photometric information for a star.
This class is used as a container for the instrumental photometry of a
star, observed in a specific photometric filter. It implements both
high-level and low-level routines, the latter of which are fundamental for
a scalable implementation of the differential photometry algorithms.
"""
def __init__(self, id_, pfilter, phot_info, times_indexes, dtype=numpy.longdouble):
"""Instantiation method for the DBStar class.
This is an abrupt descent in the abstraction ladder, but needed in
order to compute the light curves fast and minimize the memory usage.
Arguments:
id_ - the ID of the star in the LEMONdB.
pfilter - the photometric filter of the information being stored.
phot_info - a two-dimensional NumPy array with the photometric
information. It *must* have three rows (the first for the
time, the second for the magnitude and the last for the
SNR) and as many columns as records for which there is
photometric information. For example, in order to get the
magnitude of the third image, we would do phot_info[1][2]
times_indexes - a dictionary mapping each Unix time for which the star
was observed to its index in phot_info; this gives us
O(1) lookups when 'trimming' an instance. See the
BDStar.issubset and complete_for for further
information. Note that the values in this dictionary
are trusted blindly, so they better have the correct
values for phot_info!
"""
self.id = id_
self.pfilter = pfilter
if phot_info.shape[0] != 3: # number of rows
raise ValueError("'phot_info' must have exactly three rows")
self._phot_info = phot_info
self._time_indexes = times_indexes
self.dtype = dtype
def __str__(self):
""" The 'informal' string representation """
return "%s(ID = %d, filter = %s, %d records)" % (
self.__class__.__name__,
self.id,
self.pfilter,
len(self),
)
def __len__(self):
""" Return the number of records for the star """
return self._phot_info.shape[1] # number of columns
def time(self, index):
""" Return the Unix time of the index-th record """
return self._phot_info[0][index]
def _time_index(self, unix_time):
""" Return the index of the Unix time in '_phot_info' """
return self._time_indexes[unix_time]
def mag(self, index):
""" Return the magnitude of the index-th record """
return self._phot_info[1][index]
def snr(self, index):
""" Return the SNR of the index-th record """
return self._phot_info[2][index]
@property
def _unix_times(self):
""" Return the Unix times at which the star was observed """
return self._phot_info[0]
def issubset(self, other):
"""Return True if for each Unix time at which 'self' was observed,
there is also an observation for 'other'; False otherwise"""
for unix_time in self._unix_times:
if unix_time not in other._time_indexes:
return False
return True
def _trim_to(self, other):
"""Return a new DBStar which contains the records of 'self' that were
observed at the Unix times that can be found in 'other'. KeyError will
be raised if self if not a subset of other -- so you should check for
that before trimming anything"""
phot_info = numpy.empty((3, len(other)), dtype=self.dtype)
for oindex, unix_time in enumerate(other._unix_times):
sindex = self._time_index(unix_time)
phot_info[0][oindex] = self.time(sindex)
phot_info[1][oindex] = self.mag(sindex)
phot_info[2][oindex] = self.snr(sindex)
return DBStar(
self.id, self.pfilter, phot_info, other._time_indexes, dtype=self.dtype
)
def complete_for(self, iterable):
"""Iterate over the supplied DBStars and trim them.
The method returns a list with the 'trimmed' version of those DBStars
which are different than 'self' (i.e., a star instance will not be
considered to be a subset of itself) and of which it it is a subset.
"""
complete_stars = []
for star in iterable:
if self is not star and self.issubset(star):
complete_stars.append(star._trim_to(self))
return complete_stars
@staticmethod
def make_star(id_, pfilter, rows, dtype=numpy.longdouble):
"""Construct a DBstar instance for some photometric data.
Feeding the class constructor with NumPy arrays and dictionaries is not
particularly practical, so most of the time you may want to use instead
this convenience function. It also receives the star ID and the filter
of the star, but the photometric records are given as a sequence of
three-element tuples (Unix time, magnitude and SNR).
"""
# NumPy arrays are stored in contiguous blocks of memory, so adding
# rows or columns to an existing one would require to copy the entire
# array to a new block of memory. It is much better to first create an
# array as big as will be needed -- numpy.empty(), unlike zeros, does
# not initializes its entries and may therefore be marginally faster
phot_info = numpy.empty((3, len(rows)), dtype=dtype)
# A cache, mapping each Unix time to its index in phot_info; passed
# to the constructor of DBStar for O(1) lookups of Unix times
times_indexes = {}
for index, row in enumerate(rows):
unix_time, magnitude, snr = row
phot_info[0][index] = unix_time
phot_info[1][index] = magnitude
phot_info[2][index] = snr
times_indexes[unix_time] = index
return DBStar(id_, pfilter, phot_info, times_indexes, dtype=dtype)
# The parameters used for aperture photometry
typename = "PhotometricParameters"
field_names = "aperture, annulus, dannulus"
PhotometricParameters = collections.namedtuple(typename, field_names)
# A FITS image
typename = "Image"
field_names = "path pfilter unix_time object airmass gain ra dec"
Image = collections.namedtuple(typename, field_names)
class LightCurve(object):
"""The data points of a graph of light intensity of a celestial object.
Encapsulates a series of Unix times linked to a differential magnitude with
a signal-to-noise ratio. Internally stored as a list of three-element
tuples, but we are implementing the add method so that we can interact with
it as if it were a set, moving us up one level in the abstraction ladder.
"""
def __init__(self, pfilter, cstars, cweights, cstdevs, dtype=numpy.longdouble):
"""Initialize a new LightCurve object.
The 'cstars' argument is a sequence or iterable with the IDs in the
LEMONdB of the stars that were used as comparison stars when the light
curve was computed. 'cweights' is another sequence or iterable with the
corresponding weights, while 'cstdevs' contains the standard deviation
of their light curves, and from which (it is assumed) the weights were
calculated. The i-th comparison star (cstars) is assigned the i-th
weight (cweights) and standard deviation (cstdevs). The sum of all
weights should equal one.
"""
if len(cstars) != len(cweights):
msg = "number of weights must equal that of comparison stars"
raise ValueError(msg)
if not cstars:
msg = "at least one comparison star is needed"
raise ValueError(msg)
self._data = []
self.pfilter = pfilter
self.cstars = cstars
self.cweights = cweights
self.cstdevs = cstdevs
self.dtype = dtype
def add(self, unix_time, magnitude, snr):
""" Add a data point to the light curve """
self._data.append((unix_time, magnitude, snr))
def __len__(self):
return len(self._data)
def __getitem__(self, index):
return self._data[index]
def __iter__(self):
"""Return a copy of the (unix_time, magnitude, snr) tuples,
chronologically sorted"""
return iter(sorted(self._data, key=operator.itemgetter(0)))
@property
def stdev(self):
if not self:
raise ValueError("light curve is empty")
magnitudes = [mag for unix_time, mag, snr in self._data]
return numpy.std(numpy.array(magnitudes, dtype=self.dtype))
def weights(self):
"""Return a generator over the comparison stars and their weights.
This method returns a generator of three-element tuples, with (a) the
comparison star, (b) its weight and (c) the standard deviation of its
light curve, respectively.
"""
return itertools.izip(self.cstars, self.cweights, self.cstdevs)
def amplitude(self, npoints=1, median=True):
"""Compute the peak-to-peak amplitude of the light curve.
The amplitude of a light curve is usually defined, and in this manner
it is calculated by default, as the change between peak (the highest
value) and trough (lowest value). However, this method also allows to
take into account that there might be outliers, caused by measurement
errors, that could severely affect the result of this difference. Thus,
it its possible to take the mean or median of several points as the
peak and trough used to compute the amplitude. The ValueError exception
is raised if there are no points in the light cuve (i.e., it is empty).
Keyword arguments:
npoints - the number of maximum and minimum points (i.e., differential
magnitudes) that are combined to obtain the peak and trough.
median - whether the maximum and minimum points are combined taking
their median (if the parameter evaluates to True) or their
arithmetic mean (otherwise).
"""
if not self:
raise ValueError("light curve is empty")
magnitudes = sorted(mag for unix_time, mag, snr in self._data)
func = numpy.median if median else numpy.mean
return func(magnitudes[-npoints:]) - func(magnitudes[:npoints])
def ignore_noisy(self, snr):
"""Return a copy of the LightCurve without noisy points.
The method returns a deep copy of the instance from which those
differential magnitudes whose signal-to-noise ratio is below 'snr'
have been removed.
"""
curve = copy.deepcopy(self)
# _data stores three-element tuples: (Unix time, magnitude, snr)
curve._data = [x for x in curve._data if x[-1] >= snr]
return curve
class DuplicateImageError(sqlite3.IntegrityError):
""" Raised if two Images with the same Unix time are added to a LEMONdB """
pass
class DuplicateStarError(sqlite3.IntegrityError):
""" Raised if tho stars with the same ID are added to a LEMONdB """
pass
class UnknownStarError(sqlite3.IntegrityError):
""" Raised when a star foreign key constraint fails """
pass
class UnknownImageError(sqlite3.IntegrityError):
""" Raised when an image foreign key constraint fails """
pass
class DuplicatePhotometryError(sqlite3.IntegrityError):
""" Raised of more than one record for the same star and image is added"""
pass
class DuplicateLightCurvePointError(sqlite3.IntegrityError):
""" If more than one curve point for the same star and image is added"""
pass
StarInfo = collections.namedtuple(
"_StarInfo",
[
"x", # the x- and y- coordinates of the star...
"y", # ... in the image where it was detected.
"ra", # right ascension
"dec", # declination
"epoch", # astronomical epoch
"pm_ra", # proper motions in right ascension...
"pm_dec", # ... and declination.
"imag", # instrumental magnitude in the sources image.
],
)
class LEMONdB(object):
""" Interface to the SQLite database used to store our results """
def __init__(self, path, dtype=numpy.longdouble):
self.path = path
self.dtype = dtype
self.connection = sqlite3.connect(self.path, isolation_level=None)
self._cursor = self.connection.cursor()
# Enable foreign key support (SQLite >= 3.6.19)
self._execute("PRAGMA foreign_keys = ON")
self._execute("PRAGMA foreign_keys")
if not self._rows.fetchone()[0]:
raise sqlite3.NotSupportedError("foreign key support is not enabled")
self._start()
self._create_tables()
self.commit()
def _close(self):
self._cursor.close()
self.connection.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self._close()
def _execute(self, query, t=()):
""" Execute SQL query; returns nothing """
self._cursor.execute(query, t)
@property
def _rows(self):
""" Return an iterator over the rows returned by the last query """
return self._cursor
def _start(self):
""" Start a new transaction """
self._execute("BEGIN TRANSACTION")
def _end(self):
""" End the current transaction """
self._execute("END TRANSACTION")
def commit(self):
"""Make the changes of the current transaction permanent.
Automatically starts a new transaction"""
self._end()
self._start()
def _savepoint(self, name=None):
"""Start a new savepoint, use a random name if not given any.
Returns the name of the savepoint that was started."""
if not name:
name = "".join(random.sample(string.letters, 12))
self._execute("SAVEPOINT %s" % name)
return name
def _rollback_to(self, name):
""" Revert the state of the database to a savepoint """
self._execute("ROLLBACK TO %s" % name)
def _release(self, name):
"""Remove from the transaction stack all savepoints back to and
including the most recent savepoint with this name"""
self._execute("RELEASE %s" % name)
def analyze(self):
"""Run the ANALYZE command and commit automatically.
This command gathers statistics about tables and indexes and stores the
collected information in internal tables of the database where the
query optimizer can access the information and use it to help make
better query planning choices. These statistics are not automatically
updated as the content of the database changes. If the content of the
database changes significantly, or if the database schema changes, then
one should consider rerunning the ANALYZE command in order to update
the statistics. [https://www.sqlite.org/lang_analyze.html]
"""
self._execute("ANALYZE")
self.commit()
def _create_tables(self):
""" Create, if needed, the tables used by the database """
# This table will contain non-relational information about the LEMONdB
# itself: we need to store records (key-value pairs, such as ('AUTHOR',
# 'John Doe') or ('DATE', 1401993454.89).
self._execute(
"""
CREATE TABLE IF NOT EXISTS metadata (
key TEXT NOT NULL,
value BLOB,
UNIQUE (key))
"""
)
# STARS table: 'x' and 'y' are the x- and y-image coordinates of the
# stars (or, by extension, any astronomical object) in the FITS file
# on which they are detected (what we call the "sources image").
self._execute(
"""
CREATE TABLE IF NOT EXISTS stars (
id INTEGER PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
ra REAL NOT NULL,
dec REAL NOT NULL,
epoch REAL NOT NULL,
pm_ra REAL,
pm_dec REAL,
imag REAL NOT NULL)
"""
)
self._execute(
"""
CREATE TABLE IF NOT EXISTS photometric_filters (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL)
"""
)
self._execute(
"""
CREATE TABLE IF NOT EXISTS photometric_parameters (
id INTEGER PRIMARY KEY,
aperture INTEGER NOT NULL,
annulus INTEGER NOT NULL,
dannulus INTEGER NOT NULL)
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS phot_params_all_rows "
"ON photometric_parameters(aperture, annulus, dannulus)"
)
# Map (1) a set of photometric parameters and (2) a photometric filter
# to a standard deviation. This table is populated by the photometry
# module when the --annuli option is used, storing here the contents
# of the JSON file with all the candidate photometric parameters.
self._execute(
"""
CREATE TABLE IF NOT EXISTS candidate_parameters (
id INTEGER PRIMARY KEY,
pparams_id INTEGER NOT NULL,
filter_id INTEGER NOT NULL,
stdev REAL NOT NULL,
FOREIGN KEY (pparams_id) REFERENCES photometric_parameters(id),
FOREIGN KEY (filter_id) REFERENCES photometric_filters(id),
UNIQUE (pparams_id, filter_id))
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS cand_filter "
"ON candidate_parameters(filter_id)"
)
# IMAGES table: the 'sources' column stores Boolean values as integers
# 0 (False) and 1 (True), indicating the FITS image on which sources
# were detected. Only one image must have 'sources' set to True; all
# the others must be False.
self._execute(
"""
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL,
filter_id INTEGER,
unix_time REAL,
object TEXT,
airmass REAL,
gain REAL,
ra REAL NOT NULL,
dec REAL NOT NULL,
sources INTEGER NOT NULL,
FOREIGN KEY (filter_id) REFERENCES photometric_filters(id),
UNIQUE (filter_id, unix_time))
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS img_by_filter_time "
"ON images(filter_id, unix_time)"
)
# Enforce a maximum of one sources image (SOURCES == 1)
for index, when in enumerate(("INSERT", "UPDATE OF sources")):
stmt = """CREATE TRIGGER IF NOT EXISTS single_sources_%d
AFTER %s ON images
BEGIN
SELECT RAISE(ABORT, 'only one SOURCES column may be = 1')
WHERE (SELECT COUNT(*)
FROM images
WHERE sources = 1) > 1;
END; """ % (
index,
when,
)
self._execute(stmt)
# Although FILTER_ID, UNIX_TIME, AIRMASS and GAIN may be NULL, we only
# allow this for the sources image (that for which SOURCES == 1). The
# four columns are mandatory for 'normal' (so to speak) images.
for field in ("FILTER_ID", "UNIX_TIME", "AIRMASS", "GAIN"):
for index, where in enumerate(("INSERT", "UPDATE OF " + field)):
stmt = """CREATE TRIGGER IF NOT EXISTS {0}_not_null_{1}
AFTER {2} ON images
FOR EACH ROW
WHEN NEW.{0} is NULL AND NEW.sources != 1
BEGIN
SELECT RAISE(ABORT, '{0} may not be NULL unless SOURCES = 1');
END; """.format(
field, index, where
)
self._execute(stmt)
# Require RA to be in range [0, 360[
for index, when in enumerate(["INSERT", "UPDATE OF ra"]):
stmt = """CREATE TRIGGER IF NOT EXISTS ra_within_range_%d
AFTER %s ON images
FOR EACH ROW
WHEN (NEW.ra NOT BETWEEN 0 AND 360) OR (NEW.ra = 360)
BEGIN
SELECT RAISE(ABORT, 'RA out of range [0, 360[');
END; """ % (
index,
when,
)
self._execute(stmt)
# Require DEC to be in range [-90, 90]
for index, when in enumerate(["INSERT", "UPDATE OF dec"]):
stmt = """CREATE TRIGGER IF NOT EXISTS dec_within_range_%d
AFTER %s ON images
FOR EACH ROW
WHEN NEW.dec NOT BETWEEN -90 AND 90
BEGIN
SELECT RAISE(ABORT, 'DEC out of range [-90, 90]');
END; """ % (
index,
when,
)
self._execute(stmt)
# Store as a blob entire FITS files.
self._execute(
"""
CREATE TABLE IF NOT EXISTS raw_images (
id INTEGER PRIMARY KEY,
fits BLOB NOT NULL,
FOREIGN KEY (id) REFERENCES images(id))
"""
)
# For those astronomical objects with known proper motions, store the
# x- and y-coordinates where photometry was done in each image. Mostly
# useful for debugging purposes, this information allows us to verify
# that the proper-motion correction was correctly applied.
self._execute(
"""
CREATE TABLE IF NOT EXISTS pm_corrections (
id INTEGER PRIMARY KEY,
star_id INTEGER NOT NULL,
image_id INTEGER NOT NULL,
x REAL NOT NULL,
y REAL NOT NULL,
FOREIGN KEY (star_id) REFERENCES stars(id),
FOREIGN KEY (image_id) REFERENCES images(id),
UNIQUE (star_id, image_id))
"""
)
self._execute(
"""
CREATE TABLE IF NOT EXISTS photometry (
id INTEGER PRIMARY KEY,
star_id INTEGER NOT NULL,
image_id INTEGER NOT NULL,
magnitude REAL NOT NULL,
snr REAL NOT NULL,
FOREIGN KEY (star_id) REFERENCES stars(id),
FOREIGN KEY (image_id) REFERENCES images(id),
UNIQUE (star_id, image_id))
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS phot_by_star_image "
"ON photometry(star_id, image_id)"
)
self._execute(
"CREATE INDEX IF NOT EXISTS phot_by_image " "ON photometry(image_id)"
)
self._execute(
"""
CREATE TABLE IF NOT EXISTS light_curves (
id INTEGER PRIMARY KEY,
star_id INTEGER NOT NULL,
image_id INTEGER NOT NULL,
magnitude REAL NOT NULL,
snr REAL,
FOREIGN KEY (star_id) REFERENCES stars(id),
FOREIGN KEY (image_id) REFERENCES images(id),
UNIQUE (star_id, image_id))
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS curve_by_star_image "
"ON light_curves(star_id, image_id)"
)
self._execute(
"""
CREATE TABLE IF NOT EXISTS cmp_stars (
id INTEGER PRIMARY KEY,
star_id INTEGER NOT NULL,
filter_id INTEGER NOT NULL,
cstar_id INTEGER NOT NULL,
stdev REAL NOT NULL,
weight REAL NOT NULL,
FOREIGN KEY (star_id) REFERENCES stars(id),
FOREIGN KEY (filter_id) REFERENCES photometric_filters(id),
FOREIGN KEY (cstar_id) REFERENCES stars(id))
"""
)
self._execute(
"CREATE INDEX IF NOT EXISTS cstars_by_star_filter "
"ON cmp_stars(star_id, filter_id)"
)
def _table_count(self, table):
""" Return the number of rows in 'table' """
self._execute("SELECT COUNT(*) FROM %s" % table)
rows = list(self._rows) # from iterator to list
assert len(rows) == 1
return rows[0][0]
def _add_pfilter(self, pfilter):
"""Store a photometric filter in the database. The primary
key of the Passband objects in the table is their hash value"""
# Duck typing is not the way to go here. The photometric filter *must*
# be a Passband object: hash() always returns an integer, so dangerous
# (or plain wrong) things like hash(None) would go unnoticed.
if not isinstance(pfilter, passband.Passband):
msg = "'pfilter' must be a Passband object"
raise ValueError(msg)
t = (hash(pfilter), str(pfilter))
self._execute("INSERT OR IGNORE INTO photometric_filters VALUES (?, ?)", t)
@property
def _pparams_ids(self):
""" Return the ID of the photometric parameters, in ascending order"""
self._execute("SELECT id " "FROM photometric_parameters " "ORDER BY id ASC")
return list(x[0] for x in self._rows)
def _get_pparams(self, id_):
"""Return the PhotometricParamaters with this ID.
Raises KeyError if the database has nothing for this ID"""
self._execute(
"SELECT aperture, annulus, dannulus "
"FROM photometric_parameters "
"WHERE id = ?",
(id_,),
)
rows = list(self._rows)
if not rows:
raise KeyError("%d" % id_)
else:
assert len(rows) == 1
args = rows[0]
return PhotometricParameters(*args)
def _add_pparams(self, pparams):
"""Add a PhotometricParameters instance and return its ID or do
nothing and simply return the ID if already present in the database"""
t = [pparams.aperture, pparams.annulus, pparams.dannulus]
self._execute(
"SELECT id "
"FROM photometric_parameters "
" INDEXED BY phot_params_all_rows "
"WHERE aperture = ? "
" AND annulus = ? "
" AND dannulus = ?",
t,
)
try:
return list(self._rows)[0][0]
except IndexError:
t.insert(0, None)
self._execute("INSERT INTO photometric_parameters VALUES (?, ?, ?, ?)", t)
return self._cursor.lastrowid
def add_candidate_pparams(self, candidate_annuli, pfilter):
"""Store a CandidateAnnuli instance into the LEMONdB.
The method links an json_parse.CandidateAnnuli instance to a
photometric filter, adding a new record to the CANDIDATE_PARAMETERS
table. This allows us to store in the LEMONdB the photometric
parameters what were evaluated for each photometric filter, and how
good they were (the lower the standard deviation, the better). Please
refer to the documentation of the json_parse.CandidateAnnuli class and
the annuli module for further information on how the optimal parameters
for aperture photometry are identified.
Adding, for the same filter, a CandidateAnnuli with the same aperture,
annulus and dannulus (sky annulus) that a previously added object, but
a different stdev, will replace the CandidateAnnuli already in the
database. For example, if we are working with Johnson I and first add
CandidateAnnuli(1.618, 14.885, 3.236, 0.476) and, later on,
CandidateAnnuli(1.618, 14.885, 3.236, 0.981), also for Johnson I, the
former record (that with stdev 0.981) will be replaced by the latter.
"""
pparams_id = self._add_pparams(candidate_annuli)
self._add_pfilter(pfilter)
t = (None, pparams_id, hash(pfilter), candidate_annuli.stdev)
self._execute(
"INSERT OR REPLACE INTO candidate_parameters " "VALUES (?, ?, ?, ?)", t
)
def get_candidate_pparams(self, pfilter):
"""Return all the CandidateAnnuli for a photometric filter.
The method returns a list with all the CandidateAnnuli objects that
have been stored in the LEMONdB (in the CANDIDATE_PARAMETERS table,
using the add_candidate_pparams method) for the 'filter' photometric
filter. The returned CandidateAnnuli are sorted in increasing order by
their standard deviation; that is, the one with the lowest stdev goes
first, while that with the highest stdev is the last one.
"""
t = (hash(pfilter),)
self._execute(
"SELECT p.aperture, p.annulus, p.dannulus, c.stdev "
" FROM candidate_parameters AS c "
" INDEXED BY cand_filter, "
" photometric_parameters AS p "
"ON c.pparams_id = p.id "
"WHERE c.filter_id = ? "
"ORDER BY c.stdev ASC",
t,
)
return [json_parse.CandidateAnnuli(*args) for args in self._rows]
def _get_simage_id(self):
"""Return the ID of the image on which sources were detected.
Return the ID of the FITS file that was used to detect sources: it can
be identified in the IMAGES table because it is the only row where the
value of the SOURCES column is equal to one. Raises KeyError if the
sources image (LEMONdB.simage) has not yet been set.
"""
self._execute("SELECT id FROM images WHERE sources = 1")
rows = list(self._rows)
if not rows:
msg = "sources image has not yet been set"
raise KeyError(msg)
else:
assert len(rows) == 1
return rows[0][0]
@property
def simage(self):
"""Return the FITS image on which sources were detected.
Return an Image object with the information about the FITS file that
was used to detect sources. The Image.path attribute is just that, a
path, but a copy of the FITS image is also stored in the database, and
is available through the LEMONdB.mosaic attribute. Returns None if the
sources image has not yet been set.
"""
try:
id_ = self._get_simage_id()
except KeyError:
return None
t = (id_,)
self._execute(
"SELECT i.path, p.name, i.unix_time, i.object, "
" i.airmass, i.gain, i.ra, i.dec "
"FROM images AS i, photometric_filters AS p "
"ON i.filter_id = p.id "
"WHERE i.id = ?",
t,
)
rows = list(self._rows)
assert len(rows) == 1
args = list(rows[0])
args[1] = passband.Passband(args[1])
return Image(*args)
@simage.setter
def simage(self, image):
"""Set the FITS image on which sources were detected.
Receives an Image object with information about the FITS file that was
used to detect sources and stores it in the LEMONdB. The file to which
Image.path refers must exist and be readable, as it is also stored in
the database and accessible through the LEMON.mosaic attribute. If an
image with the same Unix time and photometric filter already exists in
the LEMONdB, DuplicateImageError is raised.
"""
self.add_image(image, _is_sources_img=True)
with open(image.path, "rb") as fd:
blob = fd.read()
# Get the ID of the sources image and store it as a blob
self._execute("SELECT id FROM images WHERE sources = 1")
rows = list(self._rows)
assert len(rows) == 1
id_ = rows[0][0]
t = (id_, buffer(blob))
self._execute("INSERT OR REPLACE INTO raw_images VALUES (?, ?)", t)
def add_image(self, image, _is_sources_img=False):
"""Store information about a FITS image in the database.
Raises DuplicateImageError if the Image has the same Unix time and
photometric filter that another image already stored in the LEMON
database (as these two values must be unique; i.e., we cannot have
two or more images with the same Unix time and photometric filter).
If '_is_sources_img' evaluates to True, this image is stored in the
database as the FITS file on which sources were detected, overriding
the previous sources image, if any. You are, however, expected to use
LEMONdB.simage() to achieve this. In fact, the default value of this
keyword argument should never be modified under normal circumstances
when the method is called. You are encouraged to ignore it unless you
really know what you are doing. This please-stay-away-of-it keyword
argument is undeniably inelegant, but it exists because it makes much
simpler the rest of our code, allowing us to store both 'normal' and
'sources' images in the same table and without having to rewrite a
considerable chunk of the LEMONdB class.
All the fields, except for the name of the observed object (i.e., the
'object' attribute of the Image class) are required in order to store
an image in the database: sqlite3.IntegrityError is raised in case any
of them is None, which is the data type that SQLite interprets as NULL.
As an exception to the previous statement, the photometric filter, time
of observation, airmass and gain may be None for the sources image (if
'_is_sources_img' evaluates to True). The reason for this is that,
while photometry is done on individual images, sources may be detected
on an image resulting from assembling several ones into a custom mosaic
(e.g., using the IPAC's Montage toolkit), scenario in which we cannot
properly speak about a filter, observation date, gain or airmass.
"""
# Use a SAVEPOINT to, if the insertion of the Image fails, be able
# to roll back the insertion of the photometric filter.
mark = self._savepoint()
# One of the database triggers raises sqlite3.IntegrityError if the
# photometric filter is NULL. However, we do not store the Passband
# object directly in the database, but instead use their hash value.
# Thus, the FILTER_ID column can never be NULL, since hash() always
# returns an integer. We need to raise the error ourselves.
if image.pfilter is None and not _is_sources_img:
msg = "image.pfilter may not be NULL unless SOURCES = 1"
raise sqlite3.IntegrityError(msg)
if image.pfilter:
self._add_pfilter(image.pfilter)
t = (
None,
image.path,
hash(image.pfilter) if image.pfilter else None,
image.unix_time,
image.object,
image.airmass,
image.gain,
image.ra,
image.dec,
int(_is_sources_img),
)
try:
# If this is the sources image (i.e., _is_sources_img == True), it
# will become the only one for which SOURCES == 1. Set the SOURCES
# column of the previous sources image, if any, to zero, making it
# a 'normal' image.
if _is_sources_img:
self._execute("UPDATE images SET sources = 0 WHERE sources = 1")
self._execute(
"INSERT INTO images " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", t
)
self._release(mark)
except Exception as e:
self._rollback_to(mark)
# Raise DuplicateImageError if there is already an image with this