This file is indexed.

/usr/lib/python/astrometry/sdss/common.py is in astrometry.net 0.46-0ubuntu2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
import os
from astrometry.util.fits import fits_table
from astrometry.util.miscutils import get_overlapping_region
import numpy as np
import pyfits

try:
    import cutils
except:
    cutils = None

cas_flags = dict(
    CANONICAL_CENTER = 0x0000000000000001,
    BRIGHT = 0x0000000000000002,
    EDGE = 0x0000000000000004,
    BLENDED = 0x0000000000000008,
    CHILD = 0x0000000000000010,
    PEAKCENTER = 0x0000000000000020,
    NODEBLEND = 0x0000000000000040,
    NOPROFILE = 0x0000000000000080,
    NOPETRO = 0x0000000000000100,
    MANYPETRO = 0x0000000000000200,
    NOPETRO_BIG = 0x0000000000000400,
    DEBLEND_TOO_MANY_PEAKS = 0x0000000000000800,
    COSMIC_RAY = 0x0000000000001000,
    MANYR50 = 0x0000000000002000,
    MANYR90 = 0x0000000000004000,
    BAD_RADIAL = 0x0000000000008000,
    INCOMPLETE_PROFILE = 0x0000000000010000,
    INTERP = 0x0000000000020000,
    SATURATED = 0x0000000000040000,
    NOTCHECKED = 0x0000000000080000,
    SUBTRACTED = 0x0000000000100000,
    NOSTOKES = 0x0000000000200000,
    BADSKY = 0x0000000000400000,
    PETROFAINT = 0x0000000000800000,
    TOO_LARGE = 0x0000000001000000,
    DEBLENDED_AS_PSF = 0x0000000002000000,
    DEBLEND_PRUNED = 0x0000000004000000,
    ELLIPFAINT = 0x0000000008000000,
    BINNED1 = 0x0000000010000000,
    BINNED2 = 0x0000000020000000,
    BINNED4 = 0x0000000040000000,
    MOVED = 0x0000000080000000,
    DEBLENDED_AS_MOVING = 0x0000000100000000,
    NODEBLEND_MOVING = 0x0000000200000000,
    TOO_FEW_DETECTIONS = 0x0000000400000000,
    BAD_MOVING_FIT = 0x0000000800000000,
    STATIONARY = 0x0000001000000000,
    PEAKS_TOO_CLOSE = 0x0000002000000000,
    MEDIAN_CENTER = 0x0000004000000000,
    LOCAL_EDGE = 0x0000008000000000,
    BAD_COUNTS_ERROR = 0x0000010000000000,
    BAD_MOVING_FIT_CHILD = 0x0000020000000000,
    DEBLEND_UNASSIGNED_FLUX = 0x0000040000000000,
    SATUR_CENTER = 0x0000080000000000,
    INTERP_CENTER = 0x0000100000000000,
    DEBLENDED_AT_EDGE = 0x0000200000000000,
    DEBLEND_NOPEAK = 0x0000400000000000,
    PSF_FLUX_INTERP = 0x0000800000000000,
    TOO_FEW_GOOD_DETECTIONS = 0x0001000000000000,
    CENTER_OFF_AIMAGE = 0x0002000000000000,
    DEBLEND_DEGENERATE = 0x0004000000000000,
    BRIGHTEST_GALAXY_CHILD = 0x0008000000000000,
    CANONICAL_BAND = 0x0010000000000000,
    AMOMENT_FAINT = 0x0020000000000000,
    AMOMENT_SHIFT = 0x0040000000000000,
    AMOMENT_MAXITER = 0x0080000000000000,
    MAYBE_CR = 0x0100000000000000,
    MAYBE_EGHOST = 0x0200000000000000,
    NOTCHECKED_CENTER = 0x0400000000000000,
    OBJECT2_HAS_SATUR_DN = 0x0800000000000000,
    OBJECT2_DEBLEND_PEEPHOLE = 0x1000000000000000,
    GROWN_MERGED = 0x2000000000000000,
    HAS_CENTER = 0x4000000000000000,
    RESERVED = 0x8000000000000000,
    )

# From:
# http://www.sdss3.org/svn/repo/idlutils/trunk/data/sdss/sdssMaskbits.par
# via
#     s = open('sdssMaskbits.par').read()
#     bits = []
#     for line in s.split('\n'):
#       sp = line.split()
#       line = (int(sp[2]), sp[3], ' '.join(sp[4:]))
#       bits.append(line)
#       print repr(bits).replace('), ', '),\n ')
#
photo_flags1_info = [
    # masktype OBJECT1 32 "Object flags from photo reductions for SDSS (first 32)"
    (0, 'CANONICAL_CENTER', '"The quantities (psf counts, model fits and likelihoods) that are usually determined at an object\'s center as determined band-by-band were in fact determined at the canonical center (suitably transformed). This is due to the object being to close to the edge to extract a profile at the local center, and OBJECT1_EDGE is also set."'),
    (1, 'BRIGHT', '"Indicates that the object was detected as a bright object. Since these are typically remeasured as faint objects, most users can ignore BRIGHT objects."'),
    (2, 'EDGE', '"Object is too close to edge of frame in this band."'),
    (3, 'BLENDED', '"Object was determined to be a blend. The flag is set if: more than one peak is detected within an object in a single band together; distinct peaks are found when merging different colours of one object together; or distinct peaks result when merging different objects together. "'),
    (4, 'CHILD', '"Object is a child, created by the deblender."'),
    (5, 'PEAKCENTER', '"Given center is position of peak pixel, as attempts to determine a better centroid failed."'),
    (6, 'NODEBLEND', '"Although this object was marked as a blend, no deblending was attempted."'),
    (7, 'NOPROFILE', '"Frames couldn\'t extract a radial profile."'),
    (8, 'NOPETRO', '" No Petrosian radius or other Petrosian quanties could be measured."'),
    (9, 'MANYPETRO', '"Object has more than one possible Petrosian radius."'),
    (10, 'NOPETRO_BIG', '"The Petrosian ratio has not fallen to the value at which the Petrosian radius is defined at the outermost point of the extracted radial profile. NOPETRO is set, and the Petrosian radius is set to the outermost point in the profile."'),
    (11, 'DEBLEND_TOO_MANY_PEAKS', '"The object had the OBJECT1_DEBLEND flag set, but it contained too many candidate children to be fully deblended. This flag is only set in the parent, i.e. the object with too many peaks."'),
    (12, 'CR', '"Object contains at least one pixel which was contaminated by a cosmic ray. The OBJECT1_INTERP flag is also set. This flag does not mean that this object is a cosmic ray; rather it means that a cosmic ray has been removed. "'),
    (13, 'MANYR50', '" More than one radius was found to contain 50% of the Petrosian flux. (For this to happen part of the radial profile must be negative)."'),
    (14, 'MANYR90', '"More than one radius was found to contain 90% of the Petrosian flux. (For this to happen part of the radial profile must be negative)."'),
    (15, 'BAD_RADIAL', '" Measured profile includes points with a S/N <= 0. In practice this flag is essentially meaningless."'),
    (16, 'INCOMPLETE_PROFILE', '"A circle, centerd on the object, of radius the canonical Petrosian radius extends beyond the edge of the frame. The radial profile is still measured from those parts of the object that do lie on the frame."'),
    (17, 'INTERP', '" The object contains interpolated pixels (e.g. cosmic rays or bad columns)."'),
    (18, 'SATUR', '"The object contains saturated pixels; INTERP is also set."'),
    (19, 'NOTCHECKED', '"Object includes pixels that were not checked for peaks, for example the unsmoothed edges of frames, and the cores of subtracted or saturated stars."'),
    (20, 'SUBTRACTED', '"Object (presumably a star) had wings subtracted."'),
    (21, 'NOSTOKES', '"Object has no measured Stokes parameters."'),
    (22, 'BADSKY', '"The estimated sky level is so bad that the central value of the radial profile is crazily negative; this is usually the result of the subtraction of the wings of bright stars failing."'),
    (23, 'PETROFAINT', '"At least one candidate Petrosian radius occured at an unacceptably low surface brightness."'),
    (24, 'TOO_LARGE', '" The object is (as it says) too large. Either the object is still detectable at the outermost point of the extracted radial profile (a radius of approximately 260 arcsec), or when attempting to deblend an object, at least one child is larger than half a frame (in either row or column)."'),
    (25, 'DEBLENDED_AS_PSF', '"When deblending an object, in this band this child was treated as a PSF."'),
    (26, 'DEBLEND_PRUNED', '"When solving for the weights to be assigned to each child the deblender encountered a nearly singular matrix, and therefore deleted at least one of them."'),
    (27, 'ELLIPFAINT', '"No isophotal fits were performed."'),
    (28, 'BINNED1', '"The object was detected in an unbinned image."'),
    (29, 'BINNED2', '" The object was detected in a 2x2 binned image after all unbinned detections have been replaced by the background level."'),
    (30, 'BINNED4', '"The object was detected in a 4x4 binned image. The objects detected in the 2x2 binned image are not removed before doing this."'),
    (31, 'MOVED', '"The object appears to have moved during the exposure. Such objects are candidates to be deblended as moving objects."'),
]

photo_flags2_info = [
  (0, 'DEBLENDED_AS_MOVING', '"The object has the MOVED flag set, and was deblended on the assumption that it was moving."'),
  (1, 'NODEBLEND_MOVING', '"The object has the MOVED flag set, but was not deblended as a moving object."'),
  (2, 'TOO_FEW_DETECTIONS', '"The object has the MOVED flag set, but has too few detection to be deblended as moving."'),
  (3, 'BAD_MOVING_FIT', '"The fit to the object as a moving object is too bad to be believed."'),
  (4, 'STATIONARY', '"A moving objects velocity is consistent with zero"'),
  (5, 'PEAKS_TOO_CLOSE', '"Peaks in object were too close (set only in parent objects)."'),
  (6, 'BINNED_CENTER', '"When centroiding the object the object\'s size is larger than the (PSF) filter used to smooth the image."'),
  (7, 'LOCAL_EDGE', '"The object\'s center in some band was too close to the edge of the frame to extract a profile."'),
  (8, 'BAD_COUNTS_ERROR', '"An object containing interpolated pixels had too few good pixels to form a reliable estimate of its error"'),
  (9, 'BAD_MOVING_FIT_CHILD', '"A putative moving child\'s velocity fit was too poor, so it was discarded, and the parent was not deblended as moving"'),
  (10, 'DEBLEND_UNASSIGNED_FLUX', '"After deblending, the fraction of flux assigned to none of the children was too large (this flux is then shared out as described elsewhere)."'),
  (11, 'SATUR_CENTER', '"An object\'s center is very close to at least one saturated pixel; the object may well be causing the saturation."'),
  (12, 'INTERP_CENTER', '"An object\'s center is very close to at least one interpolated pixel."'),
  (13, 'DEBLENDED_AT_EDGE', '"An object so close to the edge of the frame that it would not ordinarily be deblended has been deblended anyway. Only set for objects large enough to be EDGE in all fields/strips."'),
  (14, 'DEBLEND_NOPEAK', '"A child had no detected peak in a given band, but we centroided it anyway and set the BINNED1"'),
  (15, 'PSF_FLUX_INTERP', '"The fraction of light actually detected (as opposed to guessed at by the interpolator) was less than some number (currently 80%) of the total."'),
  (16, 'TOO_FEW_GOOD_DETECTIONS', '"A child of this object had too few good detections to be deblended as moving."'),
  (17, 'CENTER_OFF_AIMAGE', '"At least one peak\'s center lay off the atlas image in some band. This can happen when the object\'s being deblended as moving, or if the astrometry is badly confused."'),
  (18, 'DEBLEND_DEGENERATE', '"At least one potential child has been pruned because its template was too similar to some other child\'s template."'),
  (19, 'BRIGHTEST_GALAXY_CHILD', '"This is the brightest child galaxy in a blend."'),
  (20, 'CANONICAL_BAND', '"This band was the canonical band. This is the band used to measure the Petrosian radius used to calculate the Petrosian counts in each band, and to define the model used to calculate model colors; it has no effect upon the coordinate system used for the OBJC center."'),
  (21, 'AMOMENT_UNWEIGHTED', '"`Adaptive\' moments are actually unweighted."'),
  (22, 'AMOMENT_SHIFT', '"Object\'s center moved too far while determining adaptive moments. In this case, the M_e1 and M_e2 give the (row, column) shift, not the object\'s shape."'),
  (23, 'AMOMENT_MAXITER', '"Too many iterations while determining adaptive moments."'),
  (24, 'MAYBE_CR', '"This object may be a cosmic ray. This bit can get set in the cores of bright stars, and is quite likely to be set for the cores of saturated stars."'),
  (25, 'MAYBE_EGHOST', '"Object appears in the right place to be an electronics ghost."'),
  (26, 'NOTCHECKED_CENTER', '"Center of object lies in a NOTCHECKED region. The object is almost certainly bogus."'),
  (27, 'HAS_SATUR_DN', '"This object is saturated in this band and the bleed trail doesn\'t touch the edge of the frame, we we\'ve made an attempt to add up all the flux in the bleed trails, and to include it in the object\'s photometry. "'),
  (28, 'DEBLEND_PEEPHOLE', '"The deblend was modified by the optimizer"'),
  (29, 'SPARE3', '""'),
  (30, 'SPARE2', '""'),
  (31, 'SPARE1', '""'),
]


specobj_boss_target1_info = [
    # masktype BOSS_TARGET1 64 "BOSS survey primary target selection flags"
    # galaxies
    (0, 'GAL_LOZ', "low-z lrgs"),
    (1, 'GAL_CMASS', "dperp > 0.55, color-mag cut"),
    (2, 'GAL_CMASS_COMM', "dperp > 0.55, commissioning color-mag cut"),
    (3, 'GAL_CMASS_SPARSE', "GAL_CMASS_COMM & (!GAL_CMASS) & (i < 19.9) sparsely sampled"),
    (6, 'SDSS_KNOWN', "Matches a known SDSS spectra"),
    (7, 'GAL_CMASS_ALL', "GAL_CMASS and the entire sparsely sampled region"),
    (8, 'GAL_IFIBER2_FAINT', "ifiber2 > 21.5, extinction corrected. Used after Nov 2010"),
    # galaxies deprecated
    #maskbits BOSS_TARGET1 3 GAL_GRRED "red in g-r"
    #maskbits BOSS_TARGET1 4 GAL_TRIANGLE "GAL_HIZ and !GAL_CMASS"
    #maskbits BOSS_TARGET1 5 GAL_LODPERP "Same as hiz but between dperp00 and dperp0"
    # qsos (1)
    (10, 'QSO_CORE', "restrictive qso selection: commissioning only"),
    (11, 'QSO_BONUS', "permissive qso selection: commissioning only"),
    (12, 'QSO_KNOWN_MIDZ', "known qso between [2.2,9.99]"),
    (13, 'QSO_KNOWN_LOHIZ', "known qso outside of miz range. never target"),
    (14, 'QSO_NN', "Neural Net that match to sweeps/pass cuts"),
    (15, 'QSO_UKIDSS', "UKIDSS stars that match sweeps/pass flag cuts"),
    (16, 'QSO_KDE_COADD', "kde targets from the stripe82 coadd"),
    (17, 'QSO_LIKE', "likelihood method"),
    (18, 'QSO_FIRST_BOSS', "FIRST radio match"),
    (19, 'QSO_KDE', "selected by kde+chi2"),
    # standards
    (20, 'STD_FSTAR', "standard f-stars"),
    (21, 'STD_WD', "white dwarfs"),
    (22, 'STD_QSO', "qso"),
    # template stars
    (32, 'TEMPLATE_GAL_PHOTO', "galaxy templates"),
    (33, 'TEMPLATE_QSO_SDSS1', "QSO templates"),
    (34, 'TEMPLATE_STAR_PHOTO', "stellar templates"),
    (35, 'TEMPLATE_STAR_SPECTRO', "stellar templates (spectroscopically known)"),
    # qsos (2)
    (40, 'QSO_CORE_MAIN', "Main survey core sample"),
    (41, 'QSO_BONUS_MAIN', "Main survey bonus sample"),
    (42, 'QSO_CORE_ED', "Extreme Deconvolution in Core"),
    (43, 'QSO_CORE_LIKE', "Likelihood that make it into core"),
    (44, 'QSO_KNOWN_SUPPZ', "known qso between [1.8,2.15]"),
]


specobj_boss_target1_map = dict([(nm, 1<<bit)
                                 for bit,nm,desc in specobj_boss_target1_info])

photo_flags1_map = dict([(nm, 1<<bit)
                         for bit,nm,desc in photo_flags1_info])
photo_flags2_map = dict([(nm, 1<<bit)
                         for bit,nm,desc in photo_flags2_info])


def band_names():
    return ['u','g','r','i','z']

def band_name(b):
    if b in band_names():
        return b
    if b in [0,1,2,3,4]:
        return 'ugriz'[b]
    raise Exception('Invalid SDSS band: "' + str(b) + '"')

def band_index(b):
    if b in band_names():
        return 'ugriz'.index(b)
    if b in [0,1,2,3,4]:
        return b
    raise Exception('Invalid SDSS band: "' + str(b) + '"')

class SdssDR(object):
    def __init__(self, curl=False, basedir=None):
        self.curl = curl
        self.basedir = basedir
        self.filenames = {}

    def getDRNumber(self):
        return -1
        
    def getFilename(self, filetype, *args, **kwargs):
        for k,v in zip(['run', 'camcol', 'field', 'band'], args):
            kwargs[k] = v
        # convert band number to band character.
        if 'band' in kwargs and kwargs['band'] is not None:
            kwargs['band'] = band_name(kwargs['band'])
        if not filetype in self.filenames:
            return None
        pat = self.filenames[filetype]
        if not 'rerun' in kwargs:
            run = kwargs.get('run', None)
            rerun = self.get_rerun(run)
            kwargs.update(rerun=rerun)
        #print 'pat', pat, 'kwargs', kwargs
        fn = pat % kwargs
        return fn

    def get_rerun(self, run, field=None):
        return None

    def getPath(self, *args, **kwargs):
        fn = self.getFilename(*args, **kwargs)
        if fn is None:
            return None
        if self.basedir is not None:
            fn = os.path.join(self.basedir, fn)
        return fn

    def setBasedir(self, dirnm):
        self.basedir = dirnm

    def _open(self, fn):
        if self.basedir is not None:
            path = os.path.join(self.basedir, fn)
        else:
            path = fn
        return pyfits.open(path)


class SdssFile(object):
    def __init__(self, run=None, camcol=None, field=None, band=None, rerun=None,
                 **kwargs):
        '''
        band: string ('u', 'g', 'r', 'i', 'z')
        '''
        self.run = run
        self.camcol = camcol
        self.field = field
        if band is not None:
            self.band = band_name(band)
            self.bandi = band_index(band)
        if rerun is not None:
            self.rerun = rerun
        self.filetype = 'unknown'

    def getRun(self):
        return self.__dict__.get('run', 0)
    def getCamcol(self):
        return self.__dict__.get('camcol', 0)
    def getField(self):
        return self.__dict__.get('field', 0)

    def __str__(self):
        s = 'SDSS ' + self.filetype
        s += ' %i-%i-%i' % (self.getRun(), self.getCamcol(), self.getField())
        if hasattr(self, 'band'):
            s += '-%s' % self.band
        return s


def munu_to_radec_rad(mu, nu, node, incl):
    ra = node + np.arctan2(np.sin(mu - node) * np.cos(nu) * np.cos(incl) -
                           np.sin(nu) * np.sin(incl),
                           np.cos(mu - node) * np.cos(nu))
    dec = np.arcsin(np.sin(mu - node) * np.cos(nu) * np.sin(incl) +
                    np.sin(nu) * np.cos(incl))
    return ra,dec

def munu_to_radec_deg(mu, nu, node, incl):
    mu, nu = np.deg2rad(mu), np.deg2rad(nu)
    node, incl = np.deg2rad(node), np.deg2rad(incl)
    ra,dec = munu_to_radec_rad(mu, nu, node, incl)
    ra, dec = np.rad2deg(ra), np.rad2deg(dec)
    ra += (360. * (ra < 0))
    ra -= (360. * (ra > 360))
    return (ra, dec)


class AsTrans(SdssFile):
    '''
    In DR7, asTrans structures can appear in asTrans files (for a
    whole run) or in tsField files (in astrom/ or fastrom/).

    http://www.sdss.org/dr7/dm/flatFiles/asTrans.html

    In DR8, they are in asTrans files, or in the "frames".

    http://data.sdss3.org/datamodel/files/PHOTO_REDUX/RERUN/RUN/astrom/asTrans.html
    '''
    def __init__(self, *args, **kwargs):
        '''
        node, incl: in radians

        astrans: must be an object with fields:
         {a,b,c,d,e,f}[band]
         {ricut}[band]
         {drow0, drow1, drow2, drow3, dcol0, dcol1, dcol2, dcol3}[band]
         {csrow, cscol, ccrow, cccol}[band]

        cut_to_band: in DR8 frames files, the astrans elements are not arrays;
        in DR7 tsField files they are.

        Note about units in this class:

        mu,nu are in degrees (great circle coords)

        a,d are in degrees (mu0, nu0)
        b,c,e,f are in degrees/pixel (dmu,dnu/drow,dcol)
        drow0,dcol0 are in pixels (distortion coefficients order 0); dpixels
        drow1,dcol1 are unitless dpixels / pixel (distortion coefficients order 1)
        drow2,dcol2 are in 1/pixels (dpixels/pixel**2) (distortion coefficients order 2)
        drow3,dcol3 are in 1/pixels**2 (dpixels/pixel**3) (distortion coefficients order 3)
        csrow,cscol are in pixels/mag (color-dependent shift)
        ccrow,cccol are in pixels (non-color-dependent shift)
        '''
        super(AsTrans, self).__init__(*args, **kwargs)
        self.filetype = 'asTrans'
        self.node = kwargs.get('node', None)
        self.incl = kwargs.get('incl', None)
        astrans = kwargs.get('astrans', None)
        self.trans = {}
        cut = kwargs.get('cut_to_band', True)
        if astrans is not None and hasattr(self, 'bandi'):
            for f in ['a','b','c','d','e','f', 'ricut',
                      'drow0', 'drow1', 'drow2', 'drow3',
                      'dcol0', 'dcol1', 'dcol2', 'dcol3',
                      'csrow', 'cscol', 'ccrow', 'cccol']:
                try:
                    if hasattr(astrans, f):
                        el = getattr(astrans, f)
                        if cut:
                            el = el[self.bandi]
                        self.trans[f] = el
                except:
                    print 'failed to get astrans.' + f
                    import traceback
                    traceback.print_exc()
                    pass

        self._cache_vals()

    def __str__(self):
        return (SdssFile.__str__(self) +
                ' (node=%g, incl=%g)' % (self.node, self.incl))

    def _cache_vals(self):
        a, b, c, d, e, f = self._get_abcdef()
        determinant = b * f - c * e
        B =  f / determinant
        C = -c / determinant
        E = -e / determinant
        F =  b / determinant
        py,px, qy,qx = self._get_cscc()
        g0, g1, g2, g3 = self._get_drow()
        h0, h1, h2, h3 = self._get_dcol()
        color0 = self._get_ricut()
        self._cached = [self.node, self.incl,
                        a,b,c,d,e,f, B,C,E,F, px,py,qx,qy, g0,g1,g2,g3,
                        h0,h1,h2,h3, color0]

    def _get_abcdef(self):
        return tuple(self.trans[x] for x in 'abcdef')

    def _get_drow(self):
        return tuple(self.trans[x] for x in ['drow0', 'drow1', 'drow2', 'drow3'])

    def _get_dcol(self):
        return tuple(self.trans[x] for x in ['dcol0', 'dcol1', 'dcol2', 'dcol3'])

    def _get_cscc(self):
        return tuple(self.trans[x] for x in ['csrow', 'cscol', 'ccrow', 'cccol'])

    def _get_ricut(self):
        return self.trans['ricut']

    def cd_at_pixel(self, x, y, color=0):
        '''
        (x,y) to numpy array (2,2) -- the CD matrix at pixel x,y:

        [ [ dRA/dx * cos(Dec), dRA/dy * cos(Dec) ],
          [ dDec/dx          , dDec/dy           ] ]

        in FITS these are called:
        [ [ CD11             , CD12              ],
          [ CD21             , CD22              ] ]

          Note: these statements have not been verified by the FDA.
        '''
        ra0,dec0 = self.pixel_to_radec(x, y, color)
        step = 10. # pixels
        rax,decx = self.pixel_to_radec(x+step, y, color)
        ray,decy = self.pixel_to_radec(x, y+step, color)
        cosd = np.cos(np.deg2rad(dec0))
        return np.array([ [ (rax-ra0)/step * cosd, (ray-ra0)/step * cosd ],
                          [ (decx-dec0)/step     , (decy-dec0)/step      ] ])

    def pixel_to_radec(self, x, y, color=0):
        mu, nu = self.pixel_to_munu(x, y, color)
        return self.munu_to_radec(mu, nu)

    def radec_to_pixel_single_py(self, ra, dec, color=0):
        '''RA,Dec -> x,y for scalar RA,Dec.'''
        # RA,Dec -> mu,nu -> prime -> pixel
        mu, nu = self.radec_to_munu_single(ra, dec)
        #print 'py; mu,nu', mu,nu
        return self.munu_to_pixel_single(mu, nu, color)

    def radec_to_pixel_single_c(self, ra, dec):
        return cutils.radec_to_pixel(float(ra), float(dec), self._cached)

    def radec_to_pixel(self, ra, dec, color=0):
        mu, nu = self.radec_to_munu(ra, dec)
        return self.munu_to_pixel(mu, nu, color)
    
    def munu_to_pixel(self, mu, nu, color=0):
        xprime, yprime = self.munu_to_prime(mu, nu, color)
        #print 'py: xprime,yprime', xprime,yprime
        return self.prime_to_pixel(xprime, yprime, color=color)

    munu_to_pixel_single = munu_to_pixel

    def munu_to_prime(self, mu, nu, color=0):
        '''
        mu = a + b * rowm + c * colm
        nu = d + e * rowm + f * colm

        So

        [rowm; colm] = [b,c; e,f]^-1 * [mu-a; nu-d]

        [b,c; e,f]^1 = [B,C; E,F] in the code below, so

        [rowm; colm] = [B,C; E,F] * [mu-a; nu-d]

        '''
        a, b, c, d, e, f = self._get_abcdef()
        #print 'mu,nu', mu, nu, 'a,d', a,d
        determinant = b * f - c * e
        #print 'det', determinant
        B =  f / determinant
        C = -c / determinant
        E = -e / determinant
        F =  b / determinant
        #print 'B', B, 'mu-a', mu-a, 'C', C, 'nu-d', nu-d
        #print 'E', E, 'mu-a', mu-a, 'F', F, 'nu-d', nu-d
        mua = mu - a
        # in field 6955, g3, 809 we see a~413
        #if mua < -180.:
        #   mua += 360.
        mua += 360. * (mua < -180.)
        yprime = B * mua + C * (nu - d)
        xprime = E * mua + F * (nu - d)
        return xprime,yprime

    def pixel_to_munu(self, x, y, color=0):
        (xprime, yprime) = self.pixel_to_prime(x, y, color)
        a, b, c, d, e, f = self._get_abcdef()
        mu = a + b * yprime + c * xprime
        nu = d + e * yprime + f * xprime
        return (mu, nu)

    def pixel_to_prime(self, x, y, color=0):
        # Secret decoder ring:
        #  http://www.sdss.org/dr7/products/general/astrometry.html
        # (color)0 is called riCut;
        # g0, g1, g2, and g3 are called
        #    dRow0, dRow1, dRow2, and dRow3, respectively;
        # h0, h1, h2, and h3 are called
        #    dCol0, dCol1, dCol2, and dCol3, respectively;
        # px and py are called csRow and csCol, respectively;
        # and qx and qy are called ccRow and ccCol, respectively.
        color0 = self._get_ricut()
        g0, g1, g2, g3 = self._get_drow()
        h0, h1, h2, h3 = self._get_dcol()
        px, py, qx, qy = self._get_cscc()

        # #$(%*&^(%$%*& bad documentation.
        (px,py) = (py,px)
        (qx,qy) = (qy,qx)

        yprime = y + g0 + g1 * x + g2 * x**2 + g3 * x**3
        xprime = x + h0 + h1 * x + h2 * x**2 + h3 * x**3

        # The code below implements this, vectorized:
        # if color < color0:
        #   xprime += px * color
        #   yprime += py * color
        # else:
        #   xprime += qx
        #   yprime += qy
        qx = qx * np.ones_like(x)
        qy = qy * np.ones_like(y)
        #print 'color', color.shape, 'px', px.shape, 'qx', qx.shape
        #print 'color', color, 'px', px, 'py', py
        xprime += np.where(color < color0, px * color, qx)
        yprime += np.where(color < color0, py * color, qy)

        return (xprime, yprime)

    def prime_to_pixel(self, xprime, yprime,  color=0):
        color0 = self._get_ricut()
        g0, g1, g2, g3 = self._get_drow()
        h0, h1, h2, h3 = self._get_dcol()
        px, py, qx, qy = self._get_cscc()

        # #$(%*&^(%$%*& bad documentation.
        (px,py) = (py,px)
        (qx,qy) = (qy,qx)

        qx = qx * np.ones_like(xprime)
        qy = qy * np.ones_like(yprime)
        #print 'color', color.shape, 'px', px.shape, 'qx', qx.shape
        xprime -= np.where(color < color0, px * color, qx)
        yprime -= np.where(color < color0, py * color, qy)

        # Now invert:
        #   yprime = y + g0 + g1 * x + g2 * x**2 + g3 * x**3
        #   xprime = x + h0 + h1 * x + h2 * x**2 + h3 * x**3
        x = xprime - h0
        # dumb-ass Newton's method
        dx = 1.
        # FIXME -- should just update the ones that aren't zero
        # FIXME -- should put in some failsafe...
        while np.max(np.abs(np.atleast_1d(dx))) > 1e-10:
            xp    = x + h0 + h1 * x + h2 * x**2 + h3 * x**3
            dxpdx = 1 +      h1     + h2 * 2*x +  h3 * 3*x**2
            dx = (xprime - xp) / dxpdx
            #print 'Max Newton dx', max(abs(dx))
            x += dx
        y = yprime - (g0 + g1 * x + g2 * x**2 + g3 * x**3)
        return (x, y)

    def radec_to_munu_single_c(self, ra, dec):
        ''' Compute ra,dec to mu,nu for a single RA,Dec, calling C code'''
        mu,nu = cutils.radec_to_munu(ra, dec, self.node, self.incl)
        #mu2,nu2 = self.radec_to_munu(ra, dec)
        #print 'mu,mu2', mu, mu2
        #print 'nu,nu2', nu, nu2
        return mu,nu

    def radec_to_munu(self, ra, dec):
        '''
        RA,Dec in degrees

        mu,nu (great circle coords) in degrees
        '''
        node,incl = self.node, self.incl
        assert(ra is not None)
        assert(dec is not None)
        ra, dec = np.deg2rad(ra), np.deg2rad(dec)
        mu = node + np.arctan2(np.sin(ra - node) * np.cos(dec) * np.cos(incl) +
                               np.sin(dec) * np.sin(incl),
                               np.cos(ra - node) * np.cos(dec))
        nu = np.arcsin(-np.sin(ra - node) * np.cos(dec) * np.sin(incl) +
                       np.sin(dec) * np.cos(incl))
        mu, nu = np.rad2deg(mu), np.rad2deg(nu)
        mu += (360. * (mu < 0))
        mu -= (360. * (mu > 360))
        return (mu, nu)

    def munu_to_radec(self, mu, nu):
        node,incl = self.node, self.incl
        assert(mu is not None)
        assert(nu is not None)
        # just in case you thought we needed *more* rad/deg conversions...
        return munu_to_radec_deg(mu, nu, np.rad2deg(node), np.rad2deg(incl))


if cutils is not None:
    AsTrans.radec_to_munu_single = AsTrans.radec_to_munu_single_c
    AsTrans.radec_to_pixel_single = AsTrans.radec_to_pixel_single_c
else:
    AsTrans.radec_to_munu_single = AsTrans.radec_to_munu
    AsTrans.radec_to_pixel_single = AsTrans.radec_to_pixel_single_py


class TsField(SdssFile):
    def __init__(self, *args, **kwargs):
        super(TsField, self).__init__(*args, **kwargs)
        self.filetype = 'tsField'
        self.exptime = 53.907456
    def setHdus(self, p):
        self.hdus = p
        self.table = fits_table(self.hdus[1].data)[0]
        T = self.table
        self.aa = T.aa.astype(float)
        self.kk = T.kk.astype(float)
        self.airmass = T.airmass

    def getAsTrans(self, band):
        bandi = band_index(band)
        band = band_name(band)
        #node,incl = self.getNode(), self.getIncl()
        hdr = self.hdus[0].header
        node = np.deg2rad(hdr.get('NODE'))
        incl = np.deg2rad(hdr.get('INCL'))
        asTrans = AsTrans(self.run, self.camcol, self.field, band=band,
                          node=node, incl=incl, astrans=self.table)
        return asTrans

    #magL = -(2.5/ln(10))*[asinh((f/f0)/2b)+ln(b)]
    # luptitude == arcsinh mag
    # band: int
    def luptitude_to_counts(self, L, band):
        # from arcsinh softening parameters table
        #   http://www.sdss.org/dr7/algorithms/fluxcal.html#counts2mag
        b = [1.4e-10, 0.9e-10, 1.2e-10, 1.8e-10, 7.4e-10]

        b = b[band]
        maggies = 2.*b * np.sinh(-0.4 * np.log(10.) * L - np.log(b))
        dlogcounts = -0.4 * (self.aa[band] + self.kk[band] * self.airmass[band])
        return (maggies * self.exptime) * 10.**dlogcounts

    def get_zeropoint(self, band):
        return (2.5 * np.log10(self.exptime)
                -(self.aa[band] + self.kk[band] * self.airmass[band]))
        
    # band: int
    def mag_to_counts(self, mag, band):
        # log_10(counts)
        logcounts = (-0.4 * mag + np.log10(self.exptime)
                     - 0.4*(self.aa[band] + self.kk[band] * self.airmass[band]))
        #logcounts = np.minimum(logcounts, 308.)
        #print 'logcounts', logcounts
        #olderrs = np.seterr(all='print')
        rtn = 10.**logcounts
        #np.seterr(**olderrs)
        return rtn

    def counts_to_mag(self, counts, band):
        # http://www.sdss.org/dr5/algorithms/fluxcal.html#counts2mag
        # f/f0 = counts/exptime * 10**0.4*(aa + kk * airmass)
        # mag = -2.5 * log10(f/f0)
        return -2.5 * (np.log10(counts / self.exptime) +
                       0.4 * (self.aa[band] + self.kk[band] * self.airmass[band]))


    
class FpObjc(SdssFile):
    def __init__(self, *args, **kwargs):
        super(FpObjc, self).__init__(*args, **kwargs)
        self.filetype = 'fpObjc'

class FpM(SdssFile):
    def __init__(self, *args, **kwargs):
        super(FpM, self).__init__(*args, **kwargs)
        self.filetype = 'fpM'
        self.maskmap = None

    def setHdus(self, p):
        self.hdus = p

    def getMaskPlane(self, name):
        # Mask planes are described in HDU 11 (the last HDU)
        if self.maskmap is None:
            self.maskmap = {}
            T = fits_table(self.hdus[-1].data)
            #print 'Got mask definition table'
            #T.about()
            T.cut(T.defname == 'S_MASKTYPE')
            for k,v in zip(T.attributename, T.value):
                k = k.replace('S_MASK_', '')
                if k == 'S_NMASK_TYPES':
                    continue
                #print '  Mask', k, '=', v
                self.maskmap[k] = v
        if not name in self.maskmap:
            raise RuntimeError('Unknown mask plane \"%s\"' % name)

        return fits_table(self.hdus[1 + self.maskmap[name]].data)

    def setMaskedPixels(self, name, img, val, roi=None):
        M = self.getMaskPlane(name)
        if M is None:
            return
        if roi is not None:
            x0,x1,y0,y1 = roi

        for (c0,c1,r0,r1,coff,roff) in zip(M.cmin,M.cmax,M.rmin,M.rmax,
                                           M.col0, M.row0):
            assert(coff == 0)
            assert(roff == 0)
            if roi is not None:
                (outx,nil) = get_overlapping_region(c0-x0, c1+1-x0, 0, x1-x0)
                (outy,nil) = get_overlapping_region(r0-y0, r1+1-y0, 0, y1-y0)
                # print 'Mask col [%i, %i], row [%i, %i]' % (c0, c1, r0, r1)
                # print  '  outx', outx, 'outy', outy
                img[outy,outx] = val
            else:
                img[r0:r1, c0:c1] = val
        

class FpC(SdssFile):
    def __init__(self, *args, **kwargs):
        super(FpC, self).__init__(*args, **kwargs)
        self.filetype = 'fpC'
    def getImage(self):
        return self.image
    def getHeader(self):
        return self.header

class PsField(SdssFile):
    def __init__(self, *args, **kwargs):
        super(PsField, self).__init__(*args, **kwargs)
        self.filetype = 'psField'

    def setHdus(self, p):
        self.hdus = p
        t = fits_table(p[6].data)
        # the table has only one row...
        assert(len(t) == 1)
        t = t[0]
        #self.table = t
        self.gain = t.gain
        self.dark_variance = t.dark_variance
        self.sky = t.sky
        self.skyerr = t.skyerr
        self.psp_status = t.status
        # Double-Gaussian PSF params
        self.dgpsf_s1 = t.psf_sigma1_2g
        self.dgpsf_s2 = t.psf_sigma2_2g
        self.dgpsf_b  = t.psf_b_2g
        # summary PSF width (sigmas)
        self.psf_fwhm = t.psf_width * (2.*np.sqrt(2.*np.log(2.)))

        # 2-gaussian plus power-law PSF params
        self.plpsf_s1 = t.psf_sigma1
        self.plpsf_s2 = t.psf_sigma2
        self.plpsf_b = t.psf_b
        self.plpsf_p0 = t.psf_p0
        self.plpsf_beta = t.psf_beta
        self.plpsf_sigmap = t.psf_sigmap


        t = fits_table(p[8].data)
        self.per_run_apcorrs = t.ap_corr_run

    def getPowerLaw(self, bandnum):
        ''' Returns:

        (a1, sigma_1,
        a2, sigma_2,
        a3, sigma_power, beta_power)

        Where a1 is the amplitude of the first Gaussian and sigma_1 is
        its standard deviation; a2 and sigma_2 are the same for the
        second Gaussian component, and a3 is the amplitude for the
        power-law component.  Sigma is the scale length, beta the
        power.

        RHL claims:
          func = a*[exp(-x^2/(2*sigmax1^2) - y^2/(2*sigmay1^2)) +
                    b*exp(-x^2/(2*sigmax2^2) - y^2/(2*sigmay2^2)) +
                    p0*(1 + r^2/(beta*sigmap^2))^{-beta/2}]

        '''
        return (1., self.plpsf_s1[bandnum],
                self.plpsf_b[bandnum], self.plpsf_s1[bandnum],
                self.plpsf_p0[bandnum], self.plpsf_sigmap[bandnum],
                self.plpsf_beta[bandnum])

    def getPsfFwhm(self, bandnum):
        return self.psf_fwhm[bandnum]

    def getDoubleGaussian(self, bandnum, normalize=False):
        # http://www.sdss.org/dr7/dm/flatFiles/psField.html
        # good = PSP_FIELD_OK
        status = self.psp_status[bandnum]
        if status != 0:
            print 'Warning: PsField status[band=%s] =' % (bandnum), status

        # b is the "ratio of G2 to G1 at the origin", ie, not the
        # straight Gaussian amplitudes
        a  = 1.0
        s1 = self.dgpsf_s1[bandnum]
        s2 = self.dgpsf_s2[bandnum]
        b  = self.dgpsf_b[bandnum]

        # value at center is 1./(2.*pi*sigma**2)

        if normalize:
            b *= (s2/s1)**2
            absum = (a + b)
            a /= absum
            b /= absum
        
        return (float(a), float(s1), float(b), float(s2))
 
    def getEigenPsfs(self, bandnum):
        '''
        Returns a numpy array of shape, eg, (4, 51, 51).
        '''
        T = fits_table(self.hdus[bandnum+1].data)
        psfs = []
        for psf,h,w in zip(T.rrows, T.rnrow, T.rncol):
            psfs.append(psf.reshape((h,w)))
        psfs = np.array(psfs)
        return psfs

    def getEigenPolynomials(self, bandnum):
        '''
        Returns [ (xorder, yorder, coeffs),  (xorder, yorder, coeffs), ...]
        one tuple per eigen-PSF.
        xorder and yorder are np arrays of integers
        coeffs is a numpy array of floating-point coefficients
        '''
        T = fits_table(self.hdus[bandnum+1].data)
        terms = []
        for k in range(len(T)):
            nrb = T.nrow_b[k]
            ncb = T.ncol_b[k]
            c = T.c[k]
            # !!!
            c = c.copy()
            c = c.reshape(5, 5)
            c = c[:nrb,:ncb]
            (gridc,gridr) = np.meshgrid(np.arange(ncb), np.arange(nrb))
            # remove the 1e-3 coordinate prescaling
            c *= (1e-3 ** (gridr + gridc))
            I = np.flatnonzero(c)
            terms.append((gridr.flat[I], gridc.flat[I], c.flat[I]))
        return terms

    def correlateEigenPsf(self, bandnum, img):
        from scipy.ndimage.filters import correlate

        eigenpsfs = self.getEigenPsfs(bandnum)
        eigenterms = self.getEigenPolynomials(bandnum)
        H,W = img.shape
        corr = np.zeros((H,W))
        xx,yy = np.arange(W).astype(float), np.arange(H).astype(float)
        for epsf, (XO,YO,C) in zip(eigenpsfs, eigenterms):
            k = reduce(np.add, [np.outer(yy**yo, xx**xo) * c
                                for xo,yo,c in zip(XO,YO,C)])
            assert(k.shape == img.shape)
            # Trim symmetric zero-padding off the epsf.
            # This will fail spectacularly given an all-zero eigen-component.
            #print 'epsf shape:', epsf.shape
            while True:
                H,W = epsf.shape
                if (np.all(epsf[:,0] == 0) and np.all(epsf[:,-1] == 0) and
                    np.all(epsf[0,:] == 0) and np.all(epsf[-1,:] == 0)):
                    # Trim!
                    epsf = epsf[1:-1, 1:-1]
                else:
                    break
            #print 'trimmed epsf shape to:', epsf.shape
            corr += k * correlate(img, epsf)
        return corr

    def getPsfAtPoints(self, bandnum, x, y):
        '''
        Reconstruct the SDSS model PSF from KL basis functions.

        x,y can be scalars or 1-d numpy arrays.

        Return value:
        if x,y are scalars: a PSF image
        if x,y are arrays:  a list of PSF images
        '''
        rtnscalar = np.isscalar(x) and np.isscalar(y)
        x = np.atleast_1d(x).astype(float)
        y = np.atleast_1d(y).astype(float)

        eigenpsfs = self.getEigenPsfs(bandnum)
        eigenpolys = self.getEigenPolynomials(bandnum)

        # From the IDL docs:
        # http://photo.astro.princeton.edu/photoop_doc.html#SDSS_PSF_RECON
        #   acoeff_k = SUM_i{ SUM_j{ (0.001*ROWC)^i * (0.001*COLC)^j * C_k_ij } }
        #   psfimage = SUM_k{ acoeff_k * RROWS_k }

        # we assume all the eigen-psfs are the same size.
        assert(len(np.unique([psf.shape for psf in eigenpsfs])) == 1)

        xx,yy = np.broadcast_arrays(x, y)
        N = len(xx.flat)
        #print 'xx,yy', xx,yy
        psfimgs = np.zeros((N,) + eigenpsfs[0].shape)
        for epsf, (XO, YO, C) in zip(eigenpsfs, eigenpolys):
            kk = reduce(np.add, [(xx.flat ** xo) * (yy.flat ** yo) * c
                                 for (xo,yo,c) in zip(XO,YO,C)])
            #for (xo,yo,c) in zip(XO,YO,C):
            #   print ' term:', xo, yo, c, xx.flat**xo, yy.flat**yo, (xx.flat**xo) * (yy.flat**yo) * c
            #print 'kk', kk
            #print 'kk shape', kk.shape
            psfimgs += epsf[np.newaxis,:,:] * kk[:,np.newaxis,np.newaxis]

        if rtnscalar:
            return psfimgs[0,:,:]
        # convert back to a list...
        return [psfimgs[i,:,:] for i in range(N)]
        #return psfimgs


    def getGain(self, band=None):
        if band is not None:
            return self.gain[band]
        return self.gain

    def getDarkVariance(self, band=None):
        if band is not None:
            return self.dark_variance[band]
        return self.dark_variance

    def getSky(self, band=None):
        if band is not None:
            return self.sky[band]
        return self.sky

    def getSkyErr(self, band=None):
        if band is not None:
            return self.skyerr[band]
        return self.skyerr