LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
objectMasks.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (http://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 
22 import re
23 import os.path
24 import logging
25 import lsst.daf.base as dafBase
26 import lsst.geom as geom
27 import lsst.afw.table as afwTable
28 from lsst.daf.butler.formatters.file import FileFormatter
29 
30 
32  """Class to support bright object masks
33 
34  N.b. I/O is done by providing a readFits method which fools the butler.
35  """
36 
37  def __init__(self):
38  schema = afwTable.SimpleTable.makeMinimalSchema()
39  schema.addField("type", str, "type of region (e.g. box, circle)", size=10)
40  schema.addField("radius", "Angle", "radius of mask (if type == circle")
41  schema.addField("height", "Angle", "height of mask (if type == box)")
42  schema.addField("width", "Angle", "width of mask (if type == box)")
43  schema.addField("angle", "Angle", "rotation of mask (if type == box)")
44  schema.addField("mag", float, "object's magnitude")
45 
46  self._catalog_catalog = afwTable.SimpleCatalog(schema)
47  self._catalog_catalog.table.setMetadata(dafBase.PropertyList())
48 
49  self.tabletable = self._catalog_catalog.table
50  self.addNewaddNew = self._catalog_catalog.addNew
51 
52  def __len__(self):
53  return len(self._catalog_catalog)
54 
55  def __iter__(self):
56  return iter(self._catalog_catalog)
57 
58  def __getitem__(self, i):
59  return self._catalog_catalog.__getitem__(i)
60 
61  def __setitem__(self, i, v):
62  return self._catalog_catalog.__setitem__(i, v)
63 
64  @classmethod
65  def readFits(cls, fileName, hdu=0, flags=0):
66  """FitsCatalogStorage facade for `read`.
67 
68  This method is intended for use by the Gen2 Butler only.
69 
70  Parameters
71  ----------
72  fileName : `str`
73  Name of the file to read.
74  hdu : `int`
75  Provided for compatibility with the "FitsCatalogStorage" read API
76  defined in `lsst.daf.persistence`, and ignored here.
77  flags : `int`
78  Provided for compatibility with the "FitsCatalogStorage" read API
79  defined in `lsst.daf.persistence`, and ignored here.
80 
81  Notes
82  -----
83  Having a `readFits` method makes the `ObjectCatalogMask` class
84  duck-type compatible with `lsst.afw.table` catalogs, to the extent
85  needed to support reading by the Gen2 Butler with no specialized code
86  in `lsst.daf.persistence`. The on-disk type should actually be an
87  ASCII ds9 region file, typically with a ".reg" suffix.
88  """
89  return cls.readread(fileName)
90 
91  @staticmethod
92  def read(fileName):
93  """Read a ds9 region file, returning a ObjectMaskCatalog object
94 
95  The files should be structured as follows:
96 
97  # Description of catalogue as a comment
98  # CATALOG: catalog-id-string
99  # TRACT: 0
100  # PATCH: 5,4
101  # FILTER: HSC-I
102 
103  wcs; fk5
104 
105  circle(RA, DEC, RADIUS) # ID: 1, mag: 12.34
106  box(RA, DEC, XSIZE, YSIZE, THETA) # ID: 2, mag: 23.45
107  ...
108 
109  The ", mag: XX.YY" is optional
110 
111  The commented lines must be present, with the relevant fields such as
112  tract patch and filter filled in. The coordinate system must be listed
113  as above. Each patch is specified as a box or circle, with RA, DEC,
114  and dimensions specified in decimal degrees (with or without an
115  explicit "d").
116 
117  Only (axis-aligned) boxes and circles are currently supported as
118  region definitions.
119  """
120 
121  log = logging.getLogger("ObjectMaskCatalog")
122 
123  brightObjects = ObjectMaskCatalog()
124  checkedWcsIsFk5 = False
125  NaN = float("NaN")*geom.degrees
126 
127  nFormatError = 0 # number of format errors seen
128  with open(fileName) as fd:
129  for lineNo, line in enumerate(fd.readlines(), 1):
130  line = line.rstrip()
131 
132  if re.search(r"^\s*#", line):
133  #
134  # Parse any line of the form "# key : value" and put them into the metadata.
135  #
136  # The medatdata values must be defined as outlined in the above docstring
137  #
138  # The value of these three keys will be checked,
139  # so get them right!
140  #
141  mat = re.search(r"^\s*#\s*([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)", line)
142  if mat:
143  key, value = mat.group(1).lower(), mat.group(2)
144  if key == "tract":
145  value = int(value)
146 
147  brightObjects.table.getMetadata().set(key, value)
148 
149  line = re.sub(r"^\s*#.*", "", line)
150  if not line:
151  continue
152 
153  if re.search(r"^\s*wcs\s*;\s*fk5\s*$", line, re.IGNORECASE):
154  checkedWcsIsFk5 = True
155  continue
156 
157  # This regular expression parses the regions file for each region to be masked,
158  # with the format as specified in the above docstring.
159  mat = re.search(r"^\s*(box|circle)"
160  r"(?:\s+|\s*\‍(\s*)" # open paren or space
161  r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # ra + units
162  r"(?:\s+|\s*,\s*)" # sep
163  r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # dec + units
164  r"(?:\s+|\s*,\s*)" # sep
165  r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param1 + units
166  r"(?:" # start optional 1
167  r"(?:\s+|\s*,\s*)" # sep
168  r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param2 + units
169  r"(?:" # start optional 2
170  r"(?:\s+|\s*,\s*)" # sep
171  r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param3 + units
172  ")?" # end optional 2
173  ")?" # end optional 1
174  r"(?:\s*|\s*\‍)\s*)" # close paren or space
175  r"#\s*ID:[\w\s]*(\d+)" # start comment, ID
176  r"(?:\s*,?\s*mag:\s*([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?))?"
177  r"\s*$", line)
178  if mat:
179  _type, ra, raUnit, dec, decUnit, \
180  param1, param1Unit, param2, param2Unit, param3, param3Unit, \
181  _id, mag = mat.groups()
182 
183  _id = int(_id)
184  if mag is None:
185  mag = NaN
186  else:
187  mag = float(mag)
188 
189  ra = convertToAngle(ra, raUnit, "ra", fileName, lineNo)
190  dec = convertToAngle(dec, decUnit, "dec", fileName, lineNo)
191 
192  radius = NaN
193  width = NaN
194  height = NaN
195  angle = 0.0*geom.degrees
196 
197  if _type == "box":
198  width = convertToAngle(param1, param1Unit, "width", fileName, lineNo)
199  height = convertToAngle(param2, param2Unit, "height", fileName, lineNo)
200  if param3 is not None:
201  angle = convertToAngle(param3, param3Unit, "angle", fileName, lineNo)
202 
203  if angle != 0.0:
204  log.warning("Rotated boxes are not supported: \"%s\" at %s:%d",
205  line, fileName, lineNo)
206  nFormatError += 1
207  elif _type == "circle":
208  radius = convertToAngle(param1, param1Unit, "radius", fileName, lineNo)
209 
210  if not (param2 is None and param3 is None):
211  log.warning("Extra parameters for circle: \"%s\" at %s:%d",
212  line, fileName, lineNo)
213  nFormatError += 1
214 
215  rec = brightObjects.addNew()
216  # N.b. rec["coord"] = Coord is not supported, so we have to use the setter
217  rec["type"] = _type
218  rec["id"] = _id
219  rec["mag"] = mag
220  rec.setCoord(geom.SpherePoint(ra, dec))
221 
222  rec["angle"] = angle
223  rec["height"] = height
224  rec["width"] = width
225  rec["radius"] = radius
226  else:
227  log.warning("Unexpected line \"%s\" at %s:%d", line, fileName, lineNo)
228  nFormatError += 1
229 
230  if nFormatError > 0:
231  raise RuntimeError("Saw %d formatting errors in %s" % (nFormatError, fileName))
232 
233  if not checkedWcsIsFk5:
234  raise RuntimeError("Expected to see a line specifying an fk5 wcs in %s" % fileName)
235 
236  # This makes the deep copy contiguous in memory so that a ColumnView can be exposed to Numpy
237  brightObjects._catalog = brightObjects._catalog.copy(True)
238 
239  return brightObjects
240 
241 
242 def convertToAngle(var, varUnit, what, fileName, lineNo):
243  """Given a variable and its units, return an geom.Angle
244 
245  what, fileName, and lineNo are used to generate helpful error messages
246  """
247  var = float(var)
248 
249  if varUnit in ("d", "", None):
250  pass
251  elif varUnit == "'":
252  var /= 60.0
253  elif varUnit == '"':
254  var /= 3600.0
255  else:
256  raise RuntimeError("unsupported unit \"%s\" for %s at %s:%d" %
257  (varUnit, what, fileName, lineNo))
258 
259  return var*geom.degrees
260 
261 
262 class RegionFileFormatter(FileFormatter):
263  """Plugin for reading DS9 region file catalogs with Gen3 Butler.
264  """
265  extension = ".reg"
266 
267  def _readFile(self, path, pytype):
268  # Docstring inherited from FileFormatter._readFile
269  if not os.path.exists(path):
270  return None
271 
272  return pytype.read(path)
273 
274  def _writeFile(self, inMemoryDataset, fileDescriptor):
275  # Docstring inherited from FileFormatter._writeFile
276  raise NotImplementedError("Write not implemented.")
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Definition: SortedCatalog.h:42
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def readFits(cls, fileName, hdu=0, flags=0)
Definition: objectMasks.py:65
daf::base::PropertySet * set
Definition: fits.cc:912
def convertToAngle(var, varUnit, what, fileName, lineNo)
Definition: objectMasks.py:242