roman-snpit-snappl 0.11.2__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of roman-snpit-snappl might be problematic. Click here for more details.

Files changed (94) hide show
  1. {roman_snpit_snappl-0.11.2/roman_snpit_snappl.egg-info → roman_snpit_snappl-0.13.0}/PKG-INFO +1 -1
  2. roman_snpit_snappl-0.13.0/changes/63.snappl.rst +1 -0
  3. roman_snpit_snappl-0.13.0/changes/68.feature.rst +1 -0
  4. roman_snpit_snappl-0.13.0/changes/72.snappl.rst +1 -0
  5. roman_snpit_snappl-0.13.0/changes/73.feature.rst +1 -0
  6. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0/roman_snpit_snappl.egg-info}/PKG-INFO +1 -1
  7. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/roman_snpit_snappl.egg-info/SOURCES.txt +5 -0
  8. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/_version.py +3 -3
  9. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/image.py +359 -92
  10. roman_snpit_snappl-0.13.0/snappl/image_simulator.py +480 -0
  11. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/imagecollection.py +46 -7
  12. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/psf.py +230 -42
  13. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.cruft.json +0 -0
  14. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/CODEOWNERS +0 -0
  15. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +0 -0
  16. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +0 -0
  17. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/ISSUE_TEMPLATE/PR_TEMPLATE.md +0 -0
  18. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/dependabot.yml +0 -0
  19. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/labeler.yml +0 -0
  20. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/workflows/changelog.yml +0 -0
  21. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/workflows/run_labeler.yml +0 -0
  22. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/workflows/run_snappl_tests.yml +0 -0
  23. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/workflows/sphinx-deploy.yml +0 -0
  24. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.github/workflows/sub_package_update.yml +0 -0
  25. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.gitignore +0 -0
  26. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/.pre-commit-config.yaml +0 -0
  27. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/CHANGES.rst +0 -0
  28. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/CITATION.cff +0 -0
  29. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/CODE_OF_CONDUCT.md +0 -0
  30. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/CONTRIBUTING.md +0 -0
  31. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/LICENSE +0 -0
  32. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/MANIFEST.in +0 -0
  33. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/README.rst +0 -0
  34. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/.gitkeep +0 -0
  35. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/10.snappl.rst +0 -0
  36. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/13.bugfix.rst +0 -0
  37. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/14.snappl.rst +0 -0
  38. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/15.feature.rst +0 -0
  39. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/16.feature.rst +0 -0
  40. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/18.feature.rst +0 -0
  41. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/20.bugfix.rst +0 -0
  42. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/23.snappl.rst +0 -0
  43. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/26.feature.rst +0 -0
  44. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/29.feature.rst +0 -0
  45. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/3.snappl.rst +0 -0
  46. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/31.feature.rst +0 -0
  47. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/35.snappl.rst +0 -0
  48. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/36.snappl.rst +0 -0
  49. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/37.snappl.rst +0 -0
  50. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/40.snappl.rst +0 -0
  51. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/41.snappl.rst +0 -0
  52. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/43.snappl.rst +0 -0
  53. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/47.feature.rst +0 -0
  54. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/49.docs.rst +0 -0
  55. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/5.snappl.rst +0 -0
  56. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/54.snappl.rst +0 -0
  57. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/57.snappl.rst +0 -0
  58. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/58.snappl.rst +0 -0
  59. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/61.bugfix.rst +0 -0
  60. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/62.snappl.rst +0 -0
  61. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/65.bugfix.rst +0 -0
  62. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/8.snappl.rst +0 -0
  63. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/changes/9.snappl.rst +0 -0
  64. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/codespell-ignore.txt +0 -0
  65. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/Makefile +0 -0
  66. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/_static/logo_black_filled.png +0 -0
  67. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/api.rst +0 -0
  68. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/changes.rst +0 -0
  69. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/conf.py +0 -0
  70. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/index.rst +0 -0
  71. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/installation.rst +0 -0
  72. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/make.bat +0 -0
  73. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/docs/usage.rst +0 -0
  74. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/experimentation/README.md +0 -0
  75. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/experimentation/play_with_photutils.py +0 -0
  76. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/licenses/.DS_Store +0 -0
  77. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/licenses/LICENSE.rst +0 -0
  78. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/licenses/README.rst +0 -0
  79. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/licenses/TEMPLATE_LICENSE.rst +0 -0
  80. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/pyproject.toml +0 -0
  81. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/roman_snpit_snappl.egg-info/dependency_links.txt +0 -0
  82. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/roman_snpit_snappl.egg-info/not-zip-safe +0 -0
  83. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/roman_snpit_snappl.egg-info/requires.txt +0 -0
  84. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/roman_snpit_snappl.egg-info/top_level.txt +0 -0
  85. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/setup.cfg +0 -0
  86. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/setup.py +0 -0
  87. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/__init__.py +0 -0
  88. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/_dev/__init__.py +0 -0
  89. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/_dev/scm_version.py +0 -0
  90. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/data/README.rst +0 -0
  91. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/diaobject.py +0 -0
  92. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/sed.py +0 -0
  93. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/snappl/wcs.py +0 -0
  94. {roman_snpit_snappl-0.11.2 → roman_snpit_snappl-0.13.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.11.2
3
+ Version: 0.13.0
4
4
  Summary: Photometry utilities for the Roman SNPIT
5
5
  Author: Roman Supernova Project Infrastructure Team
6
6
  Maintainer-email: Roman SN PIT <raknop@lbl.gov>
@@ -0,0 +1 @@
1
+ Added the pointing, sca, mjd, and header properties to ManualFITSImages.
@@ -0,0 +1 @@
1
+ Add GaussianPSF
@@ -0,0 +1 @@
1
+ Replaced input_wcs with image
@@ -0,0 +1 @@
1
+ Add image_simulator.py ; redo image.py FITS classes ; fitsio reading/writing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.11.2
3
+ Version: 0.13.0
4
4
  Summary: Photometry utilities for the Roman SNPIT
5
5
  Author: Roman Supernova Project Infrastructure Team
6
6
  Maintainer-email: Roman SN PIT <raknop@lbl.gov>
@@ -50,7 +50,11 @@ changes/57.snappl.rst
50
50
  changes/58.snappl.rst
51
51
  changes/61.bugfix.rst
52
52
  changes/62.snappl.rst
53
+ changes/63.snappl.rst
53
54
  changes/65.bugfix.rst
55
+ changes/68.feature.rst
56
+ changes/72.snappl.rst
57
+ changes/73.feature.rst
54
58
  changes/8.snappl.rst
55
59
  changes/9.snappl.rst
56
60
  docs/Makefile
@@ -78,6 +82,7 @@ snappl/__init__.py
78
82
  snappl/_version.py
79
83
  snappl/diaobject.py
80
84
  snappl/image.py
85
+ snappl/image_simulator.py
81
86
  snappl/imagecollection.py
82
87
  snappl/psf.py
83
88
  snappl/sed.py
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.11.2'
32
- __version_tuple__ = version_tuple = (0, 11, 2)
31
+ __version__ = version = '0.13.0'
32
+ __version_tuple__ = version_tuple = (0, 13, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g3db2133f7'
34
+ __commit_id__ = commit_id = 'g57a44b882'
@@ -1,4 +1,4 @@
1
- __all__ = [ 'Image', 'Numpy2DImage', 'FITSImage', 'ManualFITSImage', 'FITSImageOnDisk',
1
+ __all__ = [ 'Image', 'Numpy2DImage', 'FITSImage', 'FITSImageStdHeaders', 'FITSImageOnDisk',
2
2
  'OpenUniverse2024FITSImage', 'RomanDatamodelImage' ]
3
3
 
4
4
  import re
@@ -7,6 +7,7 @@ import random
7
7
 
8
8
  import numpy as np
9
9
  import pandas
10
+ import fitsio
10
11
  from astropy.io import fits
11
12
  from astropy.nddata.utils import Cutout2D
12
13
  from astropy.coordinates import SkyCoord
@@ -185,6 +186,11 @@ class Image:
185
186
  """Band (str)"""
186
187
  raise NotImplementedError( f"{self.__class__.__name__} needs to implement band" )
187
188
 
189
+ @band.setter
190
+ def band(self, val):
191
+ raise NotImplementedError("{self.__class__.__name__} needs to implement band setter")
192
+
193
+
188
194
  @property
189
195
  def zeropoint( self ):
190
196
  """Image zeropoint for AB magnitudes.
@@ -482,6 +488,47 @@ class Image:
482
488
  'Photometry cancelled.' )
483
489
  raise
484
490
 
491
+ def save_data( self, which='all', path=None, noisepath=None, flagspath=None, overwrite=False ):
492
+ """Same as save; here for backwards compatibility. Use save."""
493
+ self.save( which=which, path=path, noisepath=noisepath, flagspath=flagspath, overwrite=overwrite )
494
+
495
+
496
+ def save( self, which='all', path=None, noisepath=None, flagspath=None, overwrite=False ):
497
+ """Save the image to its path(s).
498
+
499
+ May have side-effects on the internal data structure (e.g. FITS
500
+ subclasses modify the internally stored header).
501
+
502
+ Paramters
503
+ ---------
504
+ which : str, default "all"
505
+ One of 'data', 'noise', 'flags', or 'all'
506
+
507
+ path : str, default None
508
+ Path to write the image to. If not specified, will use use
509
+ self.path. Does NOT update self.path.
510
+
511
+ noisepath : str, default None
512
+ Path to write the noise image to, if the noise image is
513
+ stored as a separate image. (It isn't always; some
514
+ subclasses have it as a separate part of the data structure
515
+ that also has the image.) If None, use an internally stored
516
+ noisepath. If that is not set, and noisepath is None, and
517
+ this isn't a subclass that combines all the data planes into
518
+ one file, then any noise data array will not be written.
519
+
520
+ flagspath : str, defanot None
521
+ Path to write the flags image to, similar to noisepath.
522
+
523
+ overwrite : bool, default False
524
+ Clobber existing images?
525
+
526
+ Not implemented for all subclasses.
527
+
528
+ """
529
+ raise NotImplementedError( f"{self.__class__.__name} doesn't implement save" )
530
+
531
+
485
532
 
486
533
  # ======================================================================
487
534
  # Lots of classes will probably internally store all of data, noise, and
@@ -490,12 +537,12 @@ class Image:
490
537
  class Numpy2DImage( Image ):
491
538
  """Abstract class for classes that store their array internall as a numpy 2d array."""
492
539
 
493
- def __init__( self, *args, **kwargs ):
540
+ def __init__( self, *args, data=None, noise=None, flags=None, **kwargs ):
494
541
  super().__init__( *args, **kwargs )
495
542
 
496
- self._data = None
497
- self._noise = None
498
- self._flags = None
543
+ self._data = data
544
+ self._noise = noise
545
+ self._flags = flags
499
546
  self._image_shape = None
500
547
 
501
548
  @property
@@ -571,22 +618,74 @@ class Numpy2DImage( Image ):
571
618
 
572
619
 
573
620
  # ======================================================================
574
- # A base class for FITSImages which use an AstropyWCS wcs. Not useful
575
- # by itself, because which image you load will have different
576
- # assumptions about which HDU holds image, weight, flags, plus header
577
- # information will be different etc. However, there will be some
578
- # shared code between all FITS implementations, so that's here.
621
+ # A base class for FITSImages which use an AstropyWCS wcs. Of limited
622
+ # use by itself. Although you pass it paths, it doesn't actually
623
+ # read from paths; see FITSImageOnDisk for somethign that can.
579
624
 
580
625
  class FITSImage( Numpy2DImage ):
581
626
  """Base class for classes that read FITS images and use an AstropyWCS wcs."""
582
627
 
583
- def __init__( self, *args, **kwargs ):
628
+ def __init__( self, *args, noisepath=None, flagspath=None,
629
+ imagehdu=0, noisehdu=0, flagshdu=0, header=None, wcs=None,
630
+ std_imagenames=False, **kwargs ):
584
631
  super().__init__( *args, **kwargs )
585
632
 
586
- self._data = None
587
- self._noise = None
588
- self._flags = None
589
- self._header = None
633
+ self._header = header
634
+ self._wcs = wcs
635
+
636
+ if std_imagenames:
637
+ if any( i != 0 for i in ( imagehdu, noisehdu, flagshdu ) ):
638
+ raise ValueError( "std_imagenames requireds (image|noise|flags)hdu = 0" )
639
+ if ( noisepath is not None ) or ( flagspath is not None ):
640
+ raise ValueError( "std_imagenames can't be passed with noisepath or flagspath" )
641
+
642
+ self.imagehdu = 0
643
+ self.noisehdu = 0
644
+ self.flagshdu = 0
645
+ self.noisepath = self.path.parent / f"{self.path.name}_noise.fits"
646
+ self.flagspath = self.path.parent / f"{self.path.name}_flags.fits"
647
+ self.path = self.path.parent / f"{self.path.name}_image.fits"
648
+
649
+ else:
650
+ self.noisepath = pathlib.Path( noisepath ) if noisepath is not None else None
651
+ self.flagspath = pathlib.Path( flagspath ) if flagspath is not None else None
652
+ self.imagehdu = imagehdu
653
+ self.noisehdu = noisehdu
654
+ self.flagshdu = flagshdu
655
+
656
+
657
+ @classmethod
658
+ def _fitsio_header_to_astropy_header( cls, hdr ):
659
+ # I'm agog that astropy.io.fits.Header can't just take a fitsio HEADER
660
+ # as a constructor argument, but there you have it.
661
+
662
+ if not isinstance( hdr, fitsio.header.FITSHDR ):
663
+ raise TypeError( "_fitsio_header_to_astropy_header expects a fitsio.header.FITSHDR" )
664
+
665
+ ahdr = fits.Header()
666
+ for rec in hdr.records():
667
+ if 'comment' in rec:
668
+ ahdr[ rec['name'] ] = ( rec['value'], rec['comment'] )
669
+ else:
670
+ ahdr[ rec['name'] ] = rec['value']
671
+
672
+ return ahdr
673
+
674
+
675
+ @classmethod
676
+ def _astropy_header_to_fitsio_header( cls, ahdr ):
677
+ if not isinstance( ahdr, astropy.io.fits.header.Header ):
678
+ raise TypeError( "_astropy_header_to_fitsio_header expects a astrop.io.fits.header.Header" )
679
+
680
+ hdr = fitsio.header.FITSHDR()
681
+ for i, kw in enumerate( ahdr ):
682
+ rec = { 'name': kw, 'value': ahdr[i] }
683
+ if len( ahdr.comments[i] ) > 0:
684
+ rec['comment'] = ahdr.comments[i]
685
+ hdr.add_record( rec )
686
+
687
+ return hdr
688
+
590
689
 
591
690
  @property
592
691
  def image_shape(self):
@@ -602,8 +701,51 @@ class FITSImage( Numpy2DImage ):
602
701
 
603
702
  return self._image_shape
604
703
 
704
+ def set_fits_header( self, hdr ):
705
+ if not isinstance( hdr, fits.Header ):
706
+ raise TypeError( "FITS header must be an astropy.fits.io.header.Header" )
707
+ self._header = hdr
708
+
709
+ # Subclasses may want to replace this with something different based on how they work
605
710
  def get_fits_header( self ):
606
- raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_fits_header()" )
711
+ """Get the header of the image."""
712
+
713
+ if self._header is None:
714
+ with fitsio.FITS( self.path ) as f:
715
+ hdr = f[ self.imagehdu ].read_header()
716
+ self._header = FITSImage._fitsio_header_to_astropy_header( hdr )
717
+ return self._header
718
+
719
+
720
+ def _strip_wcs_header_keywords( self ):
721
+ """Try to strip all wcs keywords from self._header.
722
+
723
+ Useful as a pre-step for saving the image if you want to write
724
+ the WCS to the image. Using this makes sure (as best possible)
725
+ that you don't end up with conflicting WCS keywords in the
726
+ header.
727
+
728
+ This may not be complete, as it pattern matches expected keywords.
729
+ If it's missing some patterns, those won't get stripped.
730
+
731
+ """
732
+
733
+ if self._header is None:
734
+ self._header = self.get_fits_header()
735
+
736
+ basematch = re.compile( r"^C(RVAL|RPIX|UNIT|DELT|TYPE)[12]$" )
737
+ cdmatch = re.compile( r"^CD[12]_[12]$" )
738
+ sipmatch = re.compile( r"^[AB]P?_(ORDER|(\d+)_(\d+))$" )
739
+ tpvmatch = re.compile( r"^P[CV]\d+_\d+$" )
740
+
741
+ tonuke = set()
742
+ for kw in self._header.keys():
743
+ if ( basematch.search(kw) or cdmatch.search(kw) or sipmatch.search(kw) or tpvmatch.search(kw) ):
744
+ tonuke.add( kw )
745
+
746
+ for kw in tonuke:
747
+ del self._header[kw]
748
+
607
749
 
608
750
  def get_wcs( self, wcsclass=None ):
609
751
  wcsclass = "AstropyWCS" if wcsclass is None else wcsclass
@@ -616,41 +758,36 @@ class FITSImage( Numpy2DImage ):
616
758
  self._wcs = GalsimWCS.from_header( hdr )
617
759
  return self._wcs
618
760
 
619
- def get_data(self, which="all"):
761
+ def get_data( self, which="all", always_reload=False, cache=False ):
620
762
  if self._is_cutout:
621
763
  raise RuntimeError(
622
764
  "get_data called on a cutout image, this will return the ORIGINAL UNCUT image. Currently not supported."
623
765
  )
766
+
624
767
  if which not in Image.data_array_list:
625
768
  raise ValueError(f"Unknown which {which}, must be all, data, noise, or flags")
769
+ which = [ 'data', 'noise', 'flags' ] if which == 'all' else [ which ]
626
770
 
627
- if (which == "all"):
628
- if (self._data is not None) and (self._noise is not None) and (self._flags is not None):
629
- return [self._data, self._noise, self._flags]
630
- else:
631
- raise RuntimeError(
632
- f"get_data called with which='all', but not all data arrays are set. "
633
- f"Data: {self._data is not None}, Noise: {self._noise is not None},"
634
- f" Flags: {self._flags is not None}"
635
- )
636
-
637
- if (which == "data"):
638
- if (self._data is not None):
639
- return [self._data]
640
- else:
641
- raise RuntimeError("get_data called with which='data', but data is not set.")
771
+ pathmap = { 'data': self.path,
772
+ 'noise': self.noisepath,
773
+ 'flags': self.flagspath }
774
+ hdumap = { 'data': self.imagehdu,
775
+ 'noise': self.noisehdu,
776
+ 'flags': self.flagshdu }
642
777
 
643
- if (which == "noise"):
644
- if (self._noise is not None):
645
- return [self._noise]
646
- else:
647
- raise RuntimeError("get_data called with which='noise', but noise is not set.")
778
+ rval = []
779
+ for plane in which:
780
+ prop = f'_{plane}'
781
+ data = getattr( self, prop )
782
+ if always_reload or ( data is None ):
783
+ with fitsio.FITS( pathmap[plane] ) as f:
784
+ data = f[ hdumap[plane] ].read()
785
+ if cache:
786
+ setattr( self, prop, data )
787
+ rval.append( data )
788
+
789
+ return rval
648
790
 
649
- if (which == "flags"):
650
- if (self._flags is not None):
651
- return [self._flags]
652
- else:
653
- raise RuntimeError("get_data called with which='flags', but flags are not set.")
654
791
 
655
792
  def get_cutout(self, x, y, xsize, ysize=None, mode='strict', fill_value=np.nan):
656
793
  """See Image.get_cutout
@@ -708,36 +845,169 @@ class FITSImage( Numpy2DImage ):
708
845
  y = int( np.floor( y + 0.5 ) )
709
846
  return self.get_cutout( x, y, xsize, ysize, mode=mode, fill_value=fill_value )
710
847
 
848
+ def save( self, which='all', path=None, noisepath=None, flagspath=None,
849
+ imagehdu=None, noisehdu=None, flagshdu=None, overwrite=False ):
850
+ """Write image to its path. See Image.save
711
851
 
712
- # ======================================================================
713
- # ManualFITSImage
714
- #
715
- # A FITS image that doesn't know where it got its data from, you just
716
- # feed it the data.
852
+ Has the side-effect of loading self._header if it is None, and
853
+ if replacing WCS keywords in self._header with keywords from the
854
+ current image WCS.
717
855
 
718
- class ManualFITSImage(FITSImage):
719
- def __init__(self, header, data=None, noise=None, flags=None,
720
- path=None, exposure=None, sca=None, *args, **kwargs):
856
+ """
721
857
 
722
- self._data = data
723
- self._noise = noise
724
- self._flags = flags
725
- self._header = header
726
- self._wcs = None
727
- self._is_cutout = False
728
- self._image_shape = None
858
+ saveim = ( which == 'data' ) or ( which == 'all' )
859
+ saveno = ( which == 'noise' ) or ( which == 'all' )
860
+ savefl = ( which == 'flags' ) or ( which == 'all' )
861
+
862
+ imagehdu = imagehdu if imagehdu is not None else self.imagehdu
863
+ noisehdu = noisehdu if noisehdu is not None else self.noisehdu
864
+ flagshdu = flagshdu if flagshdu is not None else self.flagshdu
865
+
866
+ if ( imagehdu != 0 ) or ( noisehdu != 0 ) or ( flagshdu != 0 ):
867
+ raise NotImplementedError( "We need to implement saving to HDUs other than 0." )
868
+
869
+ path = path if path is not None else self.path
870
+ if saveim and ( path is None ):
871
+ raise RuntimeError( "Can't save data, no path." )
872
+ noisepath = noisepath if noisepath is not None else self.noisepath
873
+ if saveno and ( noisepath is None ):
874
+ raise RuntimeError( "Can't save noise, no path." )
875
+ flagspath = flagspath if flagspath is not None else self.flagspath
876
+ if savefl and ( flagspath is None ):
877
+ raise RuntimeError( "Can't save flags, no path." )
878
+
879
+ if not overwrite:
880
+ if ( path.exists() or
881
+ ( noisepath is not None and noisepath.exists() ) or
882
+ ( flagspath is not None and flagspath.exists() ) ):
883
+ raise RuntimeError( "FITSImage.save: overwrite is False, but image file(s) already exist" )
884
+ else:
885
+ if path.is_file():
886
+ path.unlink()
887
+ if ( noisepath is not None ) and ( noisepath.is_file() ):
888
+ noisepath.unlink()
889
+ if ( flagspath is not None ) and ( flagspath.is_file() ):
890
+ flagspath.unlink()
891
+
892
+ # Make sure header is loaded
893
+ self.get_fits_header()
894
+ try:
895
+ apwcs = self.get_wcs().get_astropy_wcs( readonly=True )
896
+ wcshdr = apwcs.to_header()
897
+ self._strip_wcs_header_keywords()
898
+ self._header.extend( wcshdr )
899
+ except Exception:
900
+ wcshdr = None
901
+
902
+ with fitsio.FITS( path, 'rw' ) as f:
903
+ f.write( self.data, header=FITSImage._astropy_header_to_fitsio_header( self._header ) )
904
+ if ( noisepath is not None ) and ( self.noise is not None ):
905
+ with fitsio.FITS( noisepath, 'rw' ) as f:
906
+ f.write( self.noise, header=FITSImage._astropy_header_to_fitsio_header( wcshdr ) )
907
+ if ( self.flagspath is not None ) and ( self.flags is not None ):
908
+ with fitsio.FITS( flagspath, 'rw' ) as f:
909
+ f.write( self.flags, header=FITSImage._astropy_header_to_fitsio_header( wcshdr ) )
729
910
 
730
- self.path = None
731
- self.exposure = None
732
- self.sca = None
733
911
 
734
- def get_fits_header(self):
912
+ # ======================================================================
913
+ # FITSImageStdHeaders
914
+ #
915
+ # A FITSImage that knows it has information in header keywords
916
+ # that can be configurated at instantiation time.
917
+
918
+ class FITSImageStdHeaders( FITSImage ):
919
+ def __init__( self, *args,
920
+ header_kws = {
921
+ 'band': "BAND",
922
+ 'exptime': "EXPTIME",
923
+ 'mjd': "MJD",
924
+ 'pointing': "POINTING",
925
+ 'sca': "SCA",
926
+ 'zeropoint': "ZPT" },
927
+ **kwargs ):
928
+ super().__init__( *args, **kwargs )
929
+ self._header_kws = header_kws
735
930
 
736
- """Get the header of the image."""
931
+
932
+ def get_fits_header( self ):
737
933
  if self._header is None:
738
- raise RuntimeError("Header is not set for ManualFITSImage.")
934
+ try:
935
+ self._header = FITSImage.get_fits_header( self )
936
+ except Exception:
937
+ self._header = fits.header.Header()
739
938
  return self._header
740
939
 
940
+ @property
941
+ def mjd( self ):
942
+ hdr = self.get_fits_header()
943
+ return hdr[ self._header_kws['band'] ]
944
+
945
+ @mjd.setter
946
+ def mjd( self, val ):
947
+ hdr = self.get_fits_header()
948
+ hdr[ self._header_kws['band'] ] = val
949
+
950
+ @property
951
+ def pointing( self ):
952
+ hdr = self.get_fits_header()
953
+ return hdr[ self._header_kws['pointing'] ]
954
+
955
+ @pointing.setter
956
+ def pointing( self, val ):
957
+ hdr = self.get_fits_header()
958
+ hdr[ self._header_kws['pointing'] ] = val
959
+
960
+ @property
961
+ def sca( self ):
962
+ hdr = self.get_fits_header()
963
+ return hdr[ self._header_kws['sca'] ]
964
+
965
+ @sca.setter
966
+ def sca( self, val ):
967
+ hdr = self.get_fits_header()
968
+ hdr[ self._header_kws['sca'] ] = val
969
+
970
+ @property
971
+ def band( self ):
972
+ hdr = self.get_fits_header()
973
+ return hdr[ self._header_kws['band'] ]
974
+
975
+ @band.setter
976
+ def band( self, val ):
977
+ hdr = self.get_fits_header()
978
+ hdr[ self._header_kws['band'] ] = val
979
+
980
+ @property
981
+ def zeropoint( self ):
982
+ hdr = self.get_fits_header()
983
+ return hdr[ self._header_kws['zeropoint'] ]
984
+
985
+ @zeropoint.setter
986
+ def zeropoint( self, val ):
987
+ hdr = self.get_fits_header()
988
+ hdr[ self._header_kws['zeropoint'] ] = val
989
+
990
+ @property
991
+ def mjd( self ):
992
+ hdr = self.get_fits_header()
993
+ return hdr[ self._header_kws['mjd'] ]
994
+
995
+ @mjd.setter
996
+ def mjd( self, val ):
997
+ hdr = self.get_fits_header()
998
+ hdr[ self._header_kws['mjd'] ] = val
999
+
1000
+ @property
1001
+ def exptime( self ):
1002
+ hdr = self.get_fits_header()
1003
+ return hdr[ self._header_kws['exptime'] ]
1004
+
1005
+ @exptime.setter
1006
+ def exptime( self, val ):
1007
+ hdr = self.get_fits_header()
1008
+ hdr[ self._header_kws['exptime'] ] = val
1009
+
1010
+
741
1011
 
742
1012
  # ======================================================================
743
1013
  # A class that's a FITS Image with a corresponding file or
@@ -758,15 +1028,9 @@ class FITSImageOnDisk( FITSImage ):
758
1028
 
759
1029
  """
760
1030
 
761
- def __init__( self, *args, noisepath=None, flagspath=None,
762
- imhdu=0, noisehdu=0, flagshdu=0, **kwargs ):
1031
+ def __init__( self, *args, **kwargs ):
763
1032
  super().__init__( *args, **kwargs )
764
1033
 
765
- self.noisepath = pathlib.Path( noisepath ) if noisepath is not None else None
766
- self.flagspath = pathlib.Path( flagspath ) if flagspath is not None else None
767
- self.imhdu = imhdu
768
- self.noisehdu = noisehdu
769
- self.flagshdu = flagshdu
770
1034
 
771
1035
  def uncompressed_version( self, include=[ 'data', 'noise', 'flags' ], base_dir=None ):
772
1036
  """Make sure to get a FITSImageOnDisk that's not compressed.
@@ -830,12 +1094,6 @@ class FITSImageOnDisk( FITSImage ):
830
1094
  raise TypeError( f"hdr must be a fits.header.Header, not a {type(hdr)}" )
831
1095
  self._header = hdr
832
1096
 
833
- def get_fits_header( self ):
834
- if self._header is None:
835
- with fits.open( self.path ) as hdul:
836
- self._header = hdul[ self.imhdu ].header
837
- return self._header
838
-
839
1097
  def get_data( self, which="all", always_reload=False, cache=False ):
840
1098
  if self._is_cutout:
841
1099
  raise RuntimeError( "get_data called on a cutout image, this will return the ORIGINAL UNCUT image. "
@@ -861,29 +1119,29 @@ class FITSImageOnDisk( FITSImage ):
861
1119
 
862
1120
  # Open the data, and do everything else inside that with just in case
863
1121
  # noise and flags are part of the same FITS image.
864
- with fits.open( self.path ) as imhdul:
1122
+ with fitsio.FITS( self.path ) as imagefits:
865
1123
  if "data" in toload:
866
- header = imhdul[ self.imhdu ].header
867
- data = imhdul[ self.imhdu ].data
1124
+ header = FITSImage._fitsio_header_to_astropy_header( imagefits[ self.imagehdu ].read_header() )
1125
+ data = imagefits[ self.imagehdu ].read()
868
1126
  if cache:
869
1127
  self._data = data
870
1128
  self._header = header
871
1129
 
872
1130
  if "noise" in toload:
873
- if self.noisepath is not None:
874
- with fits.open( self.noisepath ) as noihdul:
875
- noise = noihdul[ self.noisehdu ].data
1131
+ if ( self.noisepath is not None ) and ( self.noisepath != self.path ):
1132
+ with fitsio.FITS( self.noisepath ) as noisefits:
1133
+ noise = noisefits[ self.noisehdu ].read()
876
1134
  else:
877
- noise = imhdul[ self.noisehdu ].data
1135
+ noise = imagefits[ self.noisehdu ].read()
878
1136
  if cache:
879
1137
  self._noise = noise
880
1138
 
881
1139
  if "flags" in toload:
882
- if self.flagspath is not None:
883
- with fits.open( self.flagspath ) as flghdul:
884
- flags = flghdul[ self.flagshdul ].data
1140
+ if ( self.flagspath is not None ) and ( self.flagspath != self.path ):
1141
+ with fitsio.FITS( self.flagspath ) as flagsfits:
1142
+ flags = flagsfits[ self.flagshdu ].read()
885
1143
  else:
886
- flags = imhdul[ self.flagshdu ].data
1144
+ flags = imagefits[ self.flagshdu ].read()
887
1145
  if cache:
888
1146
  self._flags = flags
889
1147
 
@@ -893,14 +1151,23 @@ class FITSImageOnDisk( FITSImage ):
893
1151
  else [ data, noise, flags ] )
894
1152
 
895
1153
 
896
- def save_data( self, which="all", overwrite=False,
897
- path=None, imhdu=0,
1154
+ def save( self, which="all", overwrite=False,
1155
+ path=None, imagehdu=0,
898
1156
  noisepath=None, noisehdu=None,
899
1157
  flagspath=None, flagshdu=None ):
900
1158
  """Write the data to the file."""
901
- raise NotImplementedError( "OMG needs to be done" )
902
1159
 
1160
+ path = path if path is not None else self.path
1161
+ noisepath = noisepath if noisepath is not None else self.noisepath
1162
+ flagspath = flagspath if flagspath is not None else self.flagspath
1163
+
1164
+ if not all( ( p is None ) or ( p.name[-5:] == '.fits' ) for p in [ path, noisepath, flagspath ] ):
1165
+ raise NotImplementedError( "I don't know how to save compressed files, only files "
1166
+ "whose names end in .fits" )
903
1167
 
1168
+ FITSImage.save( self, which=which, overwrite=overwrite,
1169
+ path=path, noisepath=noisepath, flagspath=flagspath,
1170
+ imagehdu=imagehdu, noisehdu=noisehdu, flagshdu=flagshdu )
904
1171
 
905
1172
 
906
1173
  # ======================================================================
@@ -911,10 +1178,10 @@ class FITSImageOnDisk( FITSImage ):
911
1178
  # HDU 3 : DQ (32-bit integer)
912
1179
 
913
1180
  class OpenUniverse2024FITSImage( FITSImageOnDisk ):
914
- def __init__( self, *args, noisepath=None, flagspath=None, imhdu=1, noisehdu=2, flagshdu=3, **kwargs ):
1181
+ def __init__( self, *args, noisepath=None, flagspath=None, imagehdu=1, noisehdu=2, flagshdu=3, **kwargs ):
915
1182
  super().__init__( *args,
916
1183
  noisepath=noisepath, flagspath=flagspath,
917
- imhdu=imhdu, noisehdu=noisehdu, flagshdu=flagshdu,
1184
+ imagehdu=imagehdu, noisehdu=noisehdu, flagshdu=flagshdu,
918
1185
  **kwargs )
919
1186
 
920
1187
  _filenamere = re.compile( r'^Roman_TDS_simple_model_(?P<band>[^_]+)_(?P<pointing>\d+)_(?P<sca>\d+).fits' )