Skip to main content
`Ctrl`+`K`

# Source code for astropy.io.ascii.sextractor

    # Licensed under a 3-clause BSD style license - see LICENSE.rst
    """sextractor.py:
      Classes to read SExtractor table format.

    Built on daophot.py:
    :Copyright: Smithsonian Astrophysical Observatory (2011)
    :Author: Tom Aldcroft (aldcroft@head.cfa.harvard.edu)
    """

    import re

    from . import core

    class SExtractorHeader(core.BaseHeader):
        """Read the header from a file produced by SExtractor."""

        comment = r"^\s*#\s*\S\D.*"  # Find lines that don't have "# digit"

        def get_cols(self, lines):
            """
            Initialize the header Column objects from the table ``lines`` for a SExtractor
            header.  The SExtractor header is specialized so that we just copy the entire BaseHeader
            get_cols routine and modify as needed.

            Parameters
            ----------
            lines : list
                List of table lines

            """
            # This assumes that the columns are listed in order, one per line with a
            # header comment string of the format: "# 1 ID short description [unit]"
            # However, some may be missing and must be inferred from skipped column numbers
            columns = {}
            # E.g. '# 1 ID identification number' (no units) or '# 2 MAGERR magnitude of error [mag]'
            # Updated along with issue #4603, for more robust parsing of unit
            re_name_def = re.compile(
                r"""^\s* \# \s*             # possible whitespace around #
                    (?P<colnumber> [0-9]+)\s+   # number of the column in table
                    (?P<colname> [-\w]+)        # name of the column
                    # column description, match any character until...
                    (?:\s+(?P<coldescr> \w .+)
                    # ...until [non-space][space][unit] or [not-right-bracket][end]
                    (?:(?<!(\]))$|(?=(?:(?<=\S)\s+\[.+\]))))?
                    (?:\s*\[(?P<colunit>.+)\])?.* # match units in brackets
                    """,
                re.VERBOSE,
            )
            dataline = None
            for line in lines:
                if not line.startswith("#"):
                    dataline = line  # save for later to infer the actual number of columns
                    break  # End of header lines

                match = re_name_def.search(line)
                if match:
                    colnumber = int(match.group("colnumber"))
                    colname = match.group("colname")
                    coldescr = match.group("coldescr")
                    # If no units are given, colunit = None
                    colunit = match.group("colunit")
                    columns[colnumber] = (colname, coldescr, colunit)

            # Handle skipped column numbers
            colnumbers = sorted(columns)
            # Handle the case where the last column is array-like by append a pseudo column
            # If there are more data columns than the largest column number
            # then add a pseudo-column that will be dropped later.  This allows
            # the array column logic below to work in all cases.
            if dataline is not None:
                n_data_cols = len(dataline.split())
            else:
                # handles no data, where we have to rely on the last column number
                n_data_cols = colnumbers[-1]
            # sextractor column number start at 1.
            columns[n_data_cols + 1] = (None, None, None)
            colnumbers.append(n_data_cols + 1)
            if len(columns) > 1:
                # only fill in skipped columns when there is genuine column initially
                previous_column = 0
                for n in colnumbers:
                    if n != previous_column + 1:
                        for c in range(previous_column + 1, n):
                            column_name = (
                                columns[previous_column][0] + f"_{c - previous_column}"
                            )
                            column_descr = columns[previous_column][1]
                            column_unit = columns[previous_column][2]
                            columns[c] = (column_name, column_descr, column_unit)
                    previous_column = n
            # Add the columns in order to self.names
            colnumbers = sorted(columns)[:-1]  # drop the pseudo column
            self.names = []
            for n in colnumbers:
                self.names.append(columns[n][0])

            if not self.names:
                raise core.InconsistentTableError(
                    "No column names found in SExtractor header"
                )

            self.cols = []
            for n in colnumbers:
                col = core.Column(name=columns[n][0])
                col.description = columns[n][1]
                col.unit = columns[n][2]
                self.cols.append(col)

    class SExtractorData(core.BaseData):
        start_line = 0
        delimiter = " "
        comment = r"\s*#"

    class SExtractor(core.BaseReader):
        """SExtractor format table.

        SExtractor is a package for faint-galaxy photometry (Bertin & Arnouts
        1996, A&A Supp. 317, 393.)

        See: https://sextractor.readthedocs.io/en/latest/

        Example::

          # 1 NUMBER
          # 2 ALPHA_J2000
          # 3 DELTA_J2000
          # 4 FLUX_RADIUS
          # 7 MAG_AUTO [mag]
          # 8 X2_IMAGE Variance along x [pixel**2]
          # 9 X_MAMA Barycenter position along MAMA x axis [m**(-6)]
          # 10 MU_MAX Peak surface brightness above background [mag * arcsec**(-2)]
          1 32.23222 10.1211 0.8 1.2 1.4 18.1 1000.0 0.00304 -3.498
          2 38.12321 -88.1321 2.2 2.4 3.1 17.0 1500.0 0.00908 1.401

        Note the skipped numbers since flux_radius has 3 columns.  The three
        FLUX_RADIUS columns will be named FLUX_RADIUS, FLUX_RADIUS_1, FLUX_RADIUS_2
        Also note that a post-ID description (e.g. "Variance along x") is optional
        and that units may be specified at the end of a line in brackets.

        """

        _format_name = "sextractor"
        _io_registry_can_write = False
        _description = "SExtractor format table"

        header_class = SExtractorHeader
        data_class = SExtractorData
        inputter_class = core.ContinuationLinesInputter

        def read(self, table):
            """
            Read input data (file-like object, filename, list of strings, or
            single string) into a Table and return the result.
            """
            out = super().read(table)
            # remove the comments
            if "comments" in out.meta:
                del out.meta["comments"]
            return out

        def write(self, table):
            raise NotImplementedError