Beispiel #1
0
 def __init__(self, data_root, synth, fit_dir, planes=None):
     super(SFH, self).__init__()
     self.data_root = data_root
     self.synth = synth
     self.fit_dir = fit_dir
     if planes is not None:
         self._planes = planes
     else:
         self._planes = self.synth._cmds
     self.mask = Mask(self._planes)
     self._sfh_config_path = os.path.join(self.fit_dir, "sfh.dat")
     self._cmd_path = os.path.join(self.fit_dir, "cmd.txt")
     self._outfile_path = os.path.join(self.fit_dir, "output.dat")
     self._hold_path = os.path.join(self.fit_dir, "hold.dat")
     self._mask_path = os.path.join(self.fit_dir, "mask.dat")
     self._log_path = os.path.join(self.fit_dir, "sfh.log")
     self._plg_path = os.path.join(self.fit_dir, "plg.log")
     self._chi_path = os.path.join(self.fit_dir, "chi.txt")
Beispiel #2
0
class SFH(object):
    """Interface to the StarFISH ``sfh`` program.

    Parameters
    ----------
    data_root : str
        Root filename of the photometry data (the full path minus the suffix
        for each CMD plane).
    synth : :class:`synth.Synth` instance
        The instance of :class:`synth.Synth` used to prepare the synthetic
        CMDs.
    fit_dir : str
        Direcory where input files are stored for the StarFISH run.
    planes : list
        List of CMD planes, made by :class:`Synth` to use. By default all
        of the planes built by :class:`Synth` will be used.
    """
    def __init__(self, data_root, synth, fit_dir, planes=None):
        super(SFH, self).__init__()
        self.data_root = data_root
        self.synth = synth
        self.fit_dir = fit_dir
        if planes is not None:
            self._planes = planes
        else:
            self._planes = self.synth._cmds
        self.mask = Mask(self._planes)
        self._sfh_config_path = os.path.join(self.fit_dir, "sfh.dat")
        self._cmd_path = os.path.join(self.fit_dir, "cmd.txt")
        self._outfile_path = os.path.join(self.fit_dir, "output.dat")
        self._hold_path = os.path.join(self.fit_dir, "hold.dat")
        self._mask_path = os.path.join(self.fit_dir, "mask.dat")
        self._log_path = os.path.join(self.fit_dir, "sfh.log")
        self._plg_path = os.path.join(self.fit_dir, "plg.log")
        self._chi_path = os.path.join(self.fit_dir, "chi.txt")

    @property
    def outfile_path(self):
        return self._outfile_path

    @property
    def full_outfile_path(self):
        return os.path.join(starfish_dir, self.outfile_path)

    @property
    def chi_path(self):
        return self._chi_path

    @property
    def full_chi_path(self):
        return os.path.join(starfish_dir, self.chi_path)

    def run_sfh(self, hold=None):
        """Run the StarFISH `sfh` software."""
        self.synth.lockfile.write_cmdfile(self._cmd_path)
        self.synth.lockfile.write_holdfile(self._hold_path, hold=hold)
        self.mask.write(self._mask_path)
        self._write_sfh_input()
        with EnterStarFishDirectory():
            subprocess.call("./sfh < %s" % self._sfh_config_path, shell=True)

    def _write_sfh_input(self):
        """Write the SFH input file."""
        if os.path.exists(self._sfh_config_path):
            os.remove(self._sfh_config_path)

        lines = []

        # Filenames
        lines.append(self.data_root)  # datpre
        lines.append(self._cmd_path)  # cmdfile
        lines.append(self.mask.mask_path)  # maskfile
        lines.append(self._hold_path)  # hold file (needs to be created)
        lines.append(self._outfile_path)  # output
        lines.append(self._log_path)  # log
        lines.append(self._plg_path)  # plg
        lines.append(self._chi_path)  # chi

        # Synth CMD parameters
        # number of independent isochrones
        # TODO modified by the holdfile?
        lines.append(str(self.synth.n_active_groups))
        lines.append(str(len(self._planes)))

        lines.append("1")  # binning factor between synth and CMD pixels
        lines.append(str(self.synth.dpix))

        # Parameters for each CMD
        for cmd in self._planes:
            lines.append(cmd.suffix)
            lines.append("%.2f" % min(cmd.x_span))
            lines.append("%.2f" % max(cmd.x_span))
            lines.append("%.2f" % min(cmd.y_span))
            lines.append("%.2f" % max(cmd.y_span))
            nx = int((max(cmd.x_span) - min(cmd.x_span)) / self.synth.dpix)
            ny = int((max(cmd.y_span) - min(cmd.y_span)) / self.synth.dpix)
            nbox = nx * ny
            lines.append(str(nbox))

        # Runtime parameters
        # TODO enable user customization here
        lines.append("256")  # seed
        lines.append("2")  # Use Poisson fit statistic
        lines.append("0")  # don't start from a logged position
        lines.append("0")  # don't generate plg file of all tested positions
        lines.append("0")  # uniform grid
        lines.append("3")  # verbosity
        lines.append("1000.00")  # lambda; initial simplex size
        lines.append("0.68")  # error bars are at 1 sigma confidence level
        lines.append("1.000")  # threshold delta-chi**2
        lines.append("10.00")  # required parameter tolerance
        lines.append("0.0000001")  # required fit_stat tolerance
        lines.append("10000")  # number of parameter directions to search
        lines.append("3")  # number of iterations for determining errorbars

        txt = "\n".join(lines)
        with open(os.path.join(starfish_dir, self._sfh_config_path), 'w') as f:
            f.write(txt)

    def solution_table(self, avgmass=1.628,
                       marginalize_z=False, split_z=False):
        """Returns a `class`:astropy.table.Table of the derived star formation
        history.

        This is based on the ``sfh.sm`` script distributed with StarFISH.

        Parameters
        ----------
        avgmass : float
            Average mass of the stellar population; given the IMF. For a
            Salpeter IMF this is 1.628.
        marginalize_z : bool
            If ``True``, the SFH at a given time but for different
            metallicities will be coadded, resulting in a table with only
            an age dimension. This can be useful for plotting overall SFH.
        split_z : bool
            If ``True``, the return SFH will be a dictionary of tables
            corresponding to each metallicity track. Keys are logZ/Zsol strings
        """
        # read in time interval table (produced by lockfile)
        dt = self.synth.lockfile.group_dt
        print "sum of dt (Gyr)", dt.sum() / 1e9

        # TODO refactor out to its own class?
        assert marginalize_z & split_z is False

        # read sfh output
        t = Table.read(self.full_outfile_path,
                       format="ascii.no_header",
                       names=['Z', 'log(age)',
                              'amp_nstars', 'amp_nstars_n', 'amp_nstars_p'])

        # Open a photometry file to count stars
        dataset_path = os.path.join(
            starfish_dir,
            self.data_root + self._planes[0].suffix)
        _catalog = np.loadtxt(dataset_path)
        nstars = _catalog.shape[0]

        if not split_z:
            t = self._make_sfh_table(t, dt, nstars,
                                     avgmass=avgmass,
                                     marginalize_z=marginalize_z)
            return t
        else:
            tables = OrderedDict()
            z_vals = np.unique(t['Z'])
            s = np.argsort(z_vals)
            z_vals = z_vals[s]
            z_strs = ["{0:.3f}".format(np.log10(z / 0.019)) for z in z_vals]
            for z_str, z in zip(z_strs, z_vals):
                sel = np.where(t['Z'] == z)[0]
                tables[z_str] = self._make_sfh_table(t[sel], dt[sel],
                                                     nstars,
                                                     avgmass=avgmass)
            return tables

    def _make_sfh_table(self, t, dt, nstars,
                        avgmass=1.628, marginalize_z=False):
        # Renormalize to SFR (Msun/yr)
        # (Amps in the SFH file have units Nstars.)
        print len(t['amp_nstars_p'])
        print len(t['amp_nstars'])
        print len(dt)
        ep = (t['amp_nstars_p'] - t['amp_nstars']) * avgmass / dt
        en = (t['amp_nstars'] - t['amp_nstars_n']) * avgmass / dt
        sfr = t['amp_nstars'] * avgmass / dt
        mass = t['amp_nstars'] * avgmass  # solar masses produced in bin
        mass_err_neg = (t['amp_nstars'] - t['amp_nstars_n']) * avgmass
        mass_err_pos = (t['amp_nstars_p'] - t['amp_nstars']) * avgmass

        # Include Poisson errors in errorbars
        poisson_sigma = sfr / np.sqrt(nstars)
        sap = ep + poisson_sigma
        san = en + poisson_sigma
        # Truncate error bars if they extend below zero
        # so that the negative confidence region bottoms out at zero
        s = np.where((sfr - san) < 0.)[0]
        san[s] = sfr[s]

        s = np.where((mass - mass_err_neg) < 0.)[0]
        mass_err_neg[s] = mass[s]

        cmass = Column(mass, name='mass', unit='M_solar')
        cmass_neg_err = Column(mass_err_neg,
                               name='mass_neg_err', unit='M_solar')
        cmass_pos_err = Column(mass_err_pos,
                               name='mass_pos_err', unit='M_solar')
        csfr = Column(sfr, name='sfr', unit='M_solar/yr')
        csap = Column(sap, name='sfr_pos_err', unit='M_solar/yr')
        csan = Column(san, name='sfr_neg_err', unit='M_solar/yr')
        cdt = Column(dt, name='dt', unit='yr')
        t.add_columns([csfr, csap, csan, cmass, cmass_pos_err, cmass_neg_err,
                       cdt])

        if marginalize_z:
            t = marginalize_sfh_metallicity(t)

        return t

    @property
    def mean_log_age(self):
        """Mean age of a fit, in log(age)."""
        t = self.solution_table(marginalize_z=True)
        return estimate_mean_age(
            t['log(age)'], t['mass'],
            mass_positive_sigma=t['mass_pos_err'],
            mass_negative_sigma=t['mass_neg_err'],
            n_boot=1000)

    @property
    def mean_age(self):
        """Mean age of a fit, in Gyr."""
        t = self.solution_table(marginalize_z=True)
        age_gyr = 10. ** t['log(age)'] / 1e9
        return estimate_mean_age(
            age_gyr, t['mass'],
            mass_positive_sigma=t['mass_pos_err'],
            mass_negative_sigma=t['mass_neg_err'],
            n_boot=1000)

    @property
    def mean_age_by_z(self):
        """Mean age of a git in Gyr for each isochrone track."""
        sfh_tables = self.solution_table(split_z=True)
        mean_ages = OrderedDict()
        mean_age_sigmas = OrderedDict()
        for z, t in sfh_tables.iteritems():
            age_gyr = 10. ** t['log(age)'] / 1e9
            mean_age, mean_age_sigma = estimate_mean_age(
                age_gyr, t['mass'],
                mass_positive_sigma=t['mass_pos_err'],
                mass_negative_sigma=t['mass_neg_err'],
                n_boot=1000)
            mean_ages[z] = mean_age
            mean_age_sigmas[z] = mean_age_sigma
        return mean_ages, mean_age_sigmas

    def plane_index(self, plane):
        """Index of a color plane in the SFH system.

        Parameters
        ----------
        plane : :class:`starfisher.plane.ColorPlane`
            The `ColorPlane` instance to get the SFH index of.

        Returns
        -------
        index : int
            Index of the color Plane.
        """
        return self._planes.index(plane) + 1

    def read_chi(self, plane):
        """Chi-sq Hess diagram for the given plane.

        Parameters
        ----------
        plane : :class:`starfisher.plane.ColorPlane`
            The `ColorPlane` instance to get the chi-sq Hess diagram of.
        """
        data = plane.read_chi(self.full_chi_path, self.plane_index(plane))
        return data