コード例 #1
0
class ToyKernel(Kernel):
    """
    Simple toy kernel that selects healpix pixels within
    the given extension radius. Similar to 'RadialDisk'.
    """

    _params = odict(Kernel._params.items() + [
        ('extension', Parameter(0.1, [0.0001, 5.0])),
        ('nside', Parameter(4096, [4096, 4096])),
    ])

    def _cache(self, name=None):
        pixel_area = healpy.nside2pixarea(self.nside, degrees=True)
        vec = ang2vec(self.lon, self.lat)
        self.pix = query_disc(self.nside, vec, self.extension)
        self._norm = 1. / (len(self.pix) * pixel_area)

    @property
    def norm(self):
        return self._norm

    def _pdf(self, pix):
        return np.in1d(pix, self.pix)

    def pdf(self, lon, lat):
        pix = ang2pix(self.nside, lon, lat)
        return self.norm * self._pdf(pix)
コード例 #2
0
class Kernel(Model):
    """
    Base class for kernels.
    """
    _params = odict([
        ('lon', Parameter(0.0, [0.0, 360.])),
        ('lat', Parameter(0.0, [-90., 90.])),
    ])
    _mapping = odict([])
    _proj = 'ait'

    def __init__(self, proj='ait', **kwargs):
        # This __init__ is probably not necessary...
        self.proj = proj
        super(Kernel, self).__init__(**kwargs)

    def __call__(self, lon, lat):
        return self.pdf(lon, lat)

    @abstractmethod
    def _kernel(self, r):
        # unnormalized, untruncated kernel
        pass

    def _pdf(self, radius):
        # unnormalized, truncated kernel
        return np.where(radius <= self.edge, self._kernel(radius), 0.)

    @abstractmethod
    def pdf(self, lon, lat):
        # normalized, truncated pdf
        pass

    @property
    def norm(self):
        # Numerically integrate the pdf
        return 1. / self.integrate()

    @property
    def projector(self):
        if self.proj is None or self.proj.lower() == 'none':
            return None
        else:
            return Projector(self.lon, self.lat, self.proj)

    def integrate(self, rmin=0, rmax=numpy.inf):
        """
        Calculate the 2D integral of the 1D surface brightness profile 
        (i.e, the flux) between rmin and rmax (elliptical radii). 
        rmin : minimum integration radius (deg)
        rmax : maximum integration radius (deg)
        return : Solid angle integral (deg^2)
        """
        if rmin < 0: raise Exception('rmin must be >= 0')
        integrand = lambda r: self._pdf(r) * 2 * numpy.pi * r
        return scipy.integrate.quad(integrand,
                                    rmin,
                                    rmax,
                                    full_output=True,
                                    epsabs=0)[0]
コード例 #3
0
class DESDwarfs(EmpiricalPadova):
    """ Empirical isochrone derived from spectroscopic members of the
    DES dwarfs.
    """
    _params = odict([
        ('distance_modulus', Parameter(15.0, [10.0, 30.0])),
        ('age', Parameter(12.5, [12.5, 12.5])),  # Gyr
        ('metallicity', Parameter(1e-4, [1e-4, 1e-4])),
    ])

    _prefix = 'dsph'
    _basename = '%(prefix)s_a12.5_z0.00010.dat'
コード例 #4
0
class M92(EmpiricalPadova):
    """ Empirical isochrone derived from the M92 ridgeline dereddened
    and transformed to the DES system.
    """
    _params = odict([
        ('distance_modulus', Parameter(15.0, [10.0, 30.0])),
        ('age', Parameter(13.7, [13.7, 13.7])),  # Gyr
        ('metallicity', Parameter(7e-5, [7e-5, 7e-5])),
    ])

    _prefix = 'm92'
    _basename = '%(prefix)s_a13.7_z0.00007.dat'
コード例 #5
0
class Richness(Model):
    """Dummy model to hold the richness, which is not directly connected
    to either the spatial or color information and doesn't require a
    sync when updated.
    """
    _params = odict([
        ('richness', Parameter(1000.0, [0.0, np.inf])),
    ])
コード例 #6
0
class EllipticalKing(EllipticalKernel):
    """
    Stellar density distribution for King profile:
    f(r) = C * [ 1/sqrt(1 + (r/r_c)**2) - 1/sqrt(1 + (r_t/r_c)**2) ]**2
    http://adsabs.harvard.edu//abs/2006MNRAS.365.1263M (Eq. 4)
    
    The half-light radius is related to the King radius for 
    c = log10(r_t/r_c) = 0.7 :
    r_h = 1.185 * r_c
    http://adsabs.harvard.edu/abs/2010MNRAS.406.1220W (App.B)
    """
    _params = odict(
        EllipticalKernel._params.items() + 
        [
            ('truncate', Parameter(3.0, [0.0, np.inf]) ), # Truncation radius
        ])
    _mapping = odict(
        EllipticalKernel._mapping.items() +
        [
            ('r_c','extension'), # Core radius
            ('r_t','truncate'),  # Tidal radius
        ])
 
    def _kernel(self, radius):
        return ((1./np.sqrt(1.+(radius/self.r_c)**2))-(1./np.sqrt(1.+(self.r_t/self.r_c)**2)))**2

    def _cache(self, name=None):
        if name in ['extension','ellipticity','truncate']:
            self._norm = 1./self.integrate()
        else:
            return

    @property
    def norm(self):
        return self._norm
 
    @property
    def c(self):
        return np.log10(self.r_t/self.r_c)
 
    @property
    def edge(self):
        return self.r_t
コード例 #7
0
class EllipticalPlummer(EllipticalKernel):
    """
    Stellar density distribution for Plummer profile:
    f(r) = C * r_c**2 / (r_c**2 + r**2)**2
    http://adsabs.harvard.edu//abs/2006MNRAS.365.1263M (Eq. 6)
    """
    _params = odict(
        EllipticalKernel._params.items() + 
        [
            ('truncate', Parameter(3.0, [0.0, np.inf]) ), # Truncation radius
        ])
    _mapping = odict(
        EllipticalKernel._mapping.items() +
        [
            ('r_c','extension'), # Plummer radius
            ('r_h','extension'), # ADW: Depricated
            ('r_t','truncate'),  # Tidal radius
        ])
 
    def _kernel(self, radius):
        return 1./(numpy.pi*self.r_h**2 * (1.+(radius/self.r_h)**2)**2)

    def _cache(self, name=None):
        if name in [None,'extension','ellipticity','truncate']:
            self._norm = 1./self.integrate() * 1./self.jacobian
        else:
            return

    @property
    def norm(self):
        return self._norm

    @property
    def u_t(self):
        # Truncation factor
        return self.truncate/self.extension
 
    @property
    def edge(self):
        return self.r_t
コード例 #8
0
class EllipticalKernel(Kernel):
    """
    Base class for elliptical kernels.
    Ellipticity is defined as 1 - b/a where a,b are the semi-major,semi-minor
    axes respectively. The position angle is defined in degrees east of north.
    This definition follows from Martin et al. 2008:
    http://adsabs.harvard.edu/abs/2008ApJ...684.1075M

    ### This is a depricated warning (2015/08/12)
    ### ADW: WARNING!!! This is actually the PA *WEST* of North!
    ### to get the conventional PA EAST of North take 90-PA
    ### Documentation?
    """
    _params = odict(Kernel._params.items() + [
        ('extension', Parameter(0.1, [0.0001, 0.5])),
        ('ellipticity',
         Parameter(0.0, [0.0, 0.99])),  # Default 0 for RadialKernel
        ('position_angle',
         Parameter(0.0, [0.0, 180.0])),  # Default 0 for RadialKernel
        # This is the PA *WEST* of North.
        # to get the conventional PA EAST of North take 90-PA
        # Would it be better to have bounds [-90,90]?
    ])
    _mapping = odict([
        ('e', 'ellipticity'),
        ('theta', 'position_angle'),
    ])

    @property
    def norm(self):
        norm = super(EllipticalKernel, self).norm
        return norm * 1. / self.jacobian

    @property
    def jacobian(self):
        return 1. - self.e

    @property
    def a(self):
        return self.extension

    @property
    def b(self):
        return self.a * self.jacobian

    @property
    def edge(self):
        return 5. * self.extension

    def angsep(self, lon, lat):
        return angsep(self.lon, self.lat, lon, lat)

    def radius(self, lon, lat):
        x, y = self.projector.sphereToImage(lon, lat)
        costh = np.cos(np.radians(self.theta))
        sinth = np.sin(np.radians(self.theta))
        return np.sqrt(((x * costh - y * sinth) / (1 - self.e))**2 +
                       (x * sinth + y * costh)**2)

    def pdf(self, lon, lat):
        radius = self.radius(lon, lat)
        return self.norm * self._pdf(radius)

    def sample_radius(self, n):
        """
        Sample the radial distribution (deg) from the 2D stellar density.
        Output is elliptical radius in true projected coordinates.
        """
        edge = self.edge if self.edge < 20 * self.extension else 20 * self.extension
        radius = np.linspace(0, edge, 1.e5)
        pdf = self._pdf(radius) * np.sin(np.radians(radius))
        cdf = np.cumsum(pdf)
        cdf /= cdf[-1]
        fn = scipy.interpolate.interp1d(cdf, range(0, len(cdf)))
        index = numpy.floor(fn(numpy.random.uniform(size=n))).astype(int)
        return radius[index]

    def sample_lonlat(self, n):
        """
        Sample 2D distribution of points in lon, lat
        """
        # From http://en.wikipedia.org/wiki/Ellipse#General_parametric_form
        # However, Martin et al. (2009) use PA theta "from North to East"
        # Definition of phi (position angle) is offset by pi/4
        # Definition of t (eccentric anamoly) remains the same (x,y-frame usual)
        # In the end, everything is trouble because we use glon, glat...

        radius = self.sample_radius(n)
        a = radius
        b = self.jacobian * radius

        t = 2. * np.pi * numpy.random.rand(n)
        cost, sint = np.cos(t), np.sin(t)
        phi = np.pi / 2. - np.deg2rad(self.theta)
        cosphi, sinphi = np.cos(phi), np.sin(phi)
        x = a * cost * cosphi - b * sint * sinphi
        y = a * cost * sinphi + b * sint * cosphi

        if self.projector is None:
            logger.debug("Creating AITOFF projector for sampling")
            projector = Projector(self.lon, self.lat, 'ait')
        else:
            projector = self.projector
        lon, lat = projector.imageToSphere(x, y)
        return lon, lat

    simulate = sample_lonlat
    sample = sample_lonlat

    # Back-compatibility
    def setExtension(self, extension):
        self.extension = extension

    def setCenter(self, lon, lat):
        self.lon = lon
        self.lat = lat
コード例 #9
0
ファイル: model.py プロジェクト: sidneymau/ugali
class IsochroneModel(Model):
    """ Abstract base class for dealing with isochrone models. """

    _params = odict([
        ('distance_modulus', Parameter(15.0, [10.0, 30.0]) ),
        ('age',              Parameter(10.0, [0.1, 15.0]) ),  # Gyr
        ('metallicity',      Parameter(0.0002, [0.0,0.02]) ),
    ])
    _mapping = odict([
        ('mod','distance_modulus'),
        ('a','age'),                 
        ('z','metallicity'),
    ])

    # ADW: Careful, there are weird things going on with adding
    # defaults to subclasses...  When converted to a dict, the
    # last duplicate entry is filled.
    # ADW: Need to explicitly call '_cache' when updating these parameters.
    defaults = (
        ('survey','des','Name of survey filter system'),
        ('dirname',get_iso_dir(),'Directory name for isochrone files'),
        ('band_1','g','Field name for magnitude one'),
        ('band_2','r','Field name for magnitude two'),
        ('band_1_detection',True,'Band one is detection band'),
        ('imf_type','Chabrier2003','Initial mass function'),
        ('hb_stage',None,'Horizontal branch stage name'),
        ('hb_spread',0.0,'Intrinisic spread added to horizontal branch'),
        )
    
    def __init__(self, **kwargs):
        self._setup(**kwargs)
        super(IsochroneModel,self).__init__(**kwargs)

    def _setup(self, **kwargs):
        # ADW: Should we add a warning for kwargs not in defaults (and
        # thus not set)?
        defaults = odict([(d[0],d[1]) for d in self.defaults])
        [defaults.update([i]) for i in list(kwargs.items()) if i[0] in defaults]

        for k,v in list(defaults.items()):
            setattr(self,k,v)

        self.imf = ugali.analysis.imf.factory(defaults['imf_type'])
        self.index = None

    def _parse(self,filename):
        msg = "Not implemented for base class"
        raise Exception(msg)

    def get_dirname(self):
        return os.path.expandvars(self.dirname.format(survey=self.survey))

    def todict(self):
        ret = super(IsochroneModel,self).todict()
        defaults = odict([(d[0],d[1]) for d in self.defaults])
        for k,v in defaults.items():
            if getattr(self,k) != v: ret[k] = getattr(self,k)
        return ret


    @property
    def distance(self):
        """ Convert to physical distance (kpc) """
        return mod2dist(self.distance_modulus)

    def sample(self, mode='data', mass_steps=1000, mass_min=0.1, full_data_range=False):
        """Sample the isochrone in steps of mass interpolating between the
        originally defined isochrone points.

        Parameters:
        -----------
        mode : 
        mass_steps : 
        mass_min : Minimum mass [Msun]
        full_data_range :
        
        Returns:
        --------
        mass_init : Initial mass of each point
        mass_pdf : PDF of number of stars in each point
        mass_act : Actual (current mass) of each stellar point
        mag_1 : Array of magnitudes in first band (distance modulus applied)
        mag_2 : Array of magnitudes in second band (distance modulus applied)
        """

        if full_data_range:
            # ADW: Might be depricated 02/10/2015
            # Generate points over full isochrone data range
            select = slice(None)
        else:
            # Not generating points for the post-AGB stars,
            # but still count those stars towards the normalization
            select = slice(self.index)

        mass_steps = int(mass_steps)

        mass_init = self.mass_init[select]
        mass_act = self.mass_act[select]
        mag_1 = self.mag_1[select]
        mag_2 = self.mag_2[select]
        
        # ADW: Assume that the isochrones are pre-sorted by mass_init
        # This avoids some numerical instability from points that have the same
        # mass_init value (discontinuities in the isochrone).
        # ADW: Might consider using np.interp for speed
        mass_act_interpolation = scipy.interpolate.interp1d(mass_init, mass_act,assume_sorted=True)
        mag_1_interpolation = scipy.interpolate.interp1d(mass_init, mag_1,assume_sorted=True)
        mag_2_interpolation = scipy.interpolate.interp1d(mass_init, mag_2,assume_sorted=True)

        # ADW: Any other modes possible?
        if mode=='data':
            # Mass interpolation with uniform coverage between data points from isochrone file 
            mass_interpolation = scipy.interpolate.interp1d(np.arange(len(mass_init)), mass_init)
            mass_array = mass_interpolation(np.linspace(0, len(mass_init)-1, mass_steps+1))
            d_mass = mass_array[1:] - mass_array[:-1]
            mass_init_array = np.sqrt(mass_array[1:] * mass_array[:-1])
            mass_pdf_array = d_mass * self.imf.pdf(mass_init_array, log_mode=False)
            mass_act_array = mass_act_interpolation(mass_init_array)
            mag_1_array = mag_1_interpolation(mass_init_array)
            mag_2_array = mag_2_interpolation(mass_init_array)

        # Horizontal branch dispersion
        if self.hb_spread and (self.stage==self.hb_stage).any():
            logger.debug("Performing dispersion of horizontal branch...")
            mass_init_min = self.mass_init[self.stage==self.hb_stage].min()
            mass_init_max = self.mass_init[self.stage==self.hb_stage].max()
            cut = (mass_init_array>mass_init_min)&(mass_init_array<mass_init_max)
            if isinstance(self.hb_spread,collections.Iterable):
                # Explicit dispersion spacing
                dispersion_array = self.hb_spread
                n = len(dispersion_array)
            else:
                # Default dispersion spacing
                dispersion = self.hb_spread
                spacing = 0.025
                n = int(round(2.0*self.hb_spread/spacing))
                if n % 2 != 1: n += 1
                dispersion_array = np.linspace(-dispersion, dispersion, n)

            # Reset original values
            mass_pdf_array[cut] = mass_pdf_array[cut] / float(n)

            # Isochrone values for points on the HB
            mass_init_hb = mass_init_array[cut]
            mass_pdf_hb = mass_pdf_array[cut]
            mass_act_hb = mass_act_array[cut]
            mag_1_hb = mag_1_array[cut]
            mag_2_hb = mag_2_array[cut]

            # Add dispersed values
            for dispersion in dispersion_array:
                if dispersion == 0.: continue
                msg = 'Dispersion=%-.4g, HB Points=%i, Iso Points=%i'%(dispersion,cut.sum(),len(mass_init_array))
                logger.debug(msg)

                mass_init_array = np.append(mass_init_array, mass_init_hb) 
                mass_pdf_array = np.append(mass_pdf_array, mass_pdf_hb)
                mass_act_array = np.append(mass_act_array, mass_act_hb) 
                mag_1_array = np.append(mag_1_array, mag_1_hb + dispersion)
                mag_2_array = np.append(mag_2_array, mag_2_hb + dispersion)

        # Note that the mass_pdf_array is not generally normalized to unity
        # since the isochrone data range typically covers a different range
        # of initial masses
        #mass_pdf_array /= np.sum(mass_pdf_array) # ORIGINAL
        # Normalize to the number of stars in the satellite with mass > mass_min
        mass_pdf_array /= self.imf.integrate(mass_min, self.mass_init_upper_bound)
        out = np.vstack([mass_init_array,mass_pdf_array,mass_act_array,mag_1_array,mag_2_array])
        return out

    def stellar_mass(self, mass_min=0.1, steps=10000):
        """
        Compute the stellar mass (Msun; average per star). PDF comes
        from IMF, but weight by actual stellar mass.

        Parameters:
        -----------
        mass_min : Minimum mass to integrate the IMF
        steps    : Number of steps to sample the isochrone

        Returns:
        --------
        mass     : Stellar mass [Msun]
        """
        mass_max = self.mass_init_upper_bound
            
        d_log_mass = (np.log10(mass_max) - np.log10(mass_min)) / float(steps)
        log_mass = np.linspace(np.log10(mass_min), np.log10(mass_max), steps)
        mass = 10.**log_mass

        if mass_min < np.min(self.mass_init):
            mass_act_interpolation = scipy.interpolate.interp1d(np.insert(self.mass_init, 0, mass_min),
                                                                np.insert(self.mass_act, 0, mass_min))
        else:
           mass_act_interpolation = scipy.interpolate.interp1d(self.mass_init, self.mass_act) 

        mass_act = mass_act_interpolation(mass)
        return np.sum(mass_act * d_log_mass * self.imf.pdf(mass, log_mode=True))

    def stellar_luminosity(self, steps=10000):
        """
        Compute the stellar luminosity (Lsun; average per star). PDF
        comes from IMF.  The range of integration only covers the
        input isochrone data (no extrapolation used), but this seems
        like a sub-percent effect if the isochrone goes to 0.15 Msun
        for the old and metal-poor stellar populations of interest.

        Note that the stellar luminosity is very sensitive to the
        post-AGB population.

        Parameters:
        -----------
        steps : Number of steps to sample the isochrone.

        Returns:
        --------
        lum   : The stellar luminosity [Lsun]
        """
        mass_min = np.min(self.mass_init)
        mass_max = self.mass_init_upper_bound
        
        d_log_mass = (np.log10(mass_max) - np.log10(mass_min)) / float(steps)
        log_mass = np.linspace(np.log10(mass_min), np.log10(mass_max), steps)
        mass = 10.**log_mass
        
        luminosity_interpolation = scipy.interpolate.interp1d(self.mass_init, self.luminosity,fill_value=0,bounds_error=False)
        luminosity = luminosity_interpolation(mass)

        return np.sum(luminosity * d_log_mass * self.imf.pdf(mass, log_mode=True))

    def stellar_luminosity2(self, steps=10000):
        """
        DEPRECATED: ADW 2017-09-20

        Compute the stellar luminosity (L_Sol; average per star).
        Uses "sample" to generate mass sample and pdf.  The range of
        integration only covers the input isochrone data (no
        extrapolation used), but this seems like a sub-percent effect
        if the isochrone goes to 0.15 Msun for the old and metal-poor
        stellar populations of interest.

        Note that the stellar luminosity is very sensitive to the
        post-AGB population.
        """
        msg = "'%s.stellar_luminosity2': ADW 2017-09-20"%self.__class__.__name__
        DeprecationWarning(msg)
        mass_init, mass_pdf, mass_act, mag_1, mag_2 = self.sample(mass_steps=steps)
        luminosity_interpolation = scipy.interpolate.interp1d(self.mass_init, self.luminosity,fill_value=0,bounds_error=False)
        luminosity = luminosity_interpolation(mass_init)
        return np.sum(luminosity * mass_pdf)

    # ADW: For temporary backward compatibility
    stellarMass = stellar_mass
    stellarLuminosity = stellar_luminosity

    def absolute_magnitude(self, richness=1, steps=1e4):
        """
        Calculate the absolute magnitude (Mv) by integrating the
        stellar luminosity.

        Parameters:
        -----------
        richness : isochrone normalization parameter
        steps    : number of isochrone sampling steps

        Returns:
        --------
        abs_mag : Absolute magnitude (Mv)
        """
        # Using the SDSS g,r -> V from Jester 2005 [arXiv:0506022]
        # for stars with R-I < 1.15
        # V = g_sdss - 0.59(g_sdss-r_sdss) - 0.01
        # g_des = g_sdss - 0.104(g_sdss - r_sdss) + 0.01
        # r_des = r_sdss - 0.102(g_sdss - r_sdss) + 0.02
        if self.survey.lower() != 'des':
            raise Exception('Only valid for DES')
        if 'g' not in [self.band_1,self.band_2]:
            msg = "Need g-band for absolute magnitude"
            raise Exception(msg)    
        if 'r' not in [self.band_1,self.band_2]:
            msg = "Need r-band for absolute magnitude"
            raise Exception(msg)    

        mass_init,mass_pdf,mass_act,mag_1,mag_2=self.sample(mass_steps = steps)
        g,r = (mag_1,mag_2) if self.band_1 == 'g' else (mag_2,mag_1)
        
        V = g - 0.487*(g - r) - 0.0249
        flux = np.sum(mass_pdf*10**(-V/2.5))
        Mv = -2.5*np.log10(richness*flux)
        return Mv

    def absolute_magnitude_martin(self, richness=1, steps=1e4, n_trials=1000, mag_bright=16., mag_faint=23., alpha=0.32, seed=None):
        """
        Calculate the absolute magnitude (Mv) of the isochrone using
        the prescription of Martin et al. 2008.
        
        Parameters:
        -----------
        richness : Isochrone nomalization factor
        steps : Number of steps for sampling the isochrone.
        n_trials : Number of bootstrap samples
        mag_bright : Bright magnitude limit for calculating luminosity.
        mag_faint : Faint magnitude limit for calculating luminosity.
        alpha : Output confidence interval (1-alpha)
        seed : Random seed

        Returns:
        --------
        med,lo,hi : Absolute magnitude interval
        """
        # ADW: This function is not quite right. You should be restricting
        # the catalog to the obsevable space (using the function named as such)
        # Also, this needs to be applied in each pixel individually
        
        # Using the SDSS g,r -> V from Jester 2005 [arXiv:0506022]
        # for stars with R-I < 1.15
        # V = g_sdss - 0.59(g_sdss-r_sdss) - 0.01
        # g_des = g_sdss - 0.104(g_sdss - r_sdss) + 0.01
        # r_des = r_sdss - 0.102(g_sdss - r_sdss) + 0.02
        np.random.seed(seed)

        if self.survey.lower() != 'des':
            raise Exception('Only valid for DES')
        if 'g' not in [self.band_1,self.band_2]:
            msg = "Need g-band for absolute magnitude"
            raise Exception(msg)    
        if 'r' not in [self.band_1,self.band_2]:
            msg = "Need r-band for absolute magnitude"
            raise Exception(msg)    
        
        def visual(g, r, pdf=None):
            v = g - 0.487 * (g - r) - 0.0249
            if pdf is None:
                flux = np.sum(10**(-v / 2.5))
            else:
                flux = np.sum(pdf * 10**(-v / 2.5))
            abs_mag_v = -2.5 * np.log10(flux)
            return abs_mag_v

        def sumMag(mag_1, mag_2):
            flux_1 = 10**(-mag_1 / 2.5)
            flux_2 = 10**(-mag_2 / 2.5)
            return -2.5 * np.log10(flux_1 + flux_2)

        # Analytic part
        mass_init, mass_pdf, mass_act, mag_1, mag_2 = self.sample(mass_steps = steps)
        g,r = (mag_1,mag_2) if self.band_1 == 'g' else (mag_2,mag_1)
        #cut = np.logical_not((g > mag_bright) & (g < mag_faint) & (r > mag_bright) & (r < mag_faint))
        cut = ((g + self.distance_modulus) > mag_faint) if self.band_1 == 'g' else ((r + self.distance_modulus) > mag_faint)
        mag_unobs = visual(g[cut], r[cut], richness * mass_pdf[cut])

        # Stochastic part
        abs_mag_obs_array = np.zeros(n_trials)
        for ii in range(0, n_trials):
            if ii%100==0: logger.debug('%i absolute magnitude trials'%ii)
            g, r = self.simulate(richness * self.stellar_mass())
            #cut = (g > 16.) & (g < 23.) & (r > 16.) & (r < 23.)
            cut = (g < mag_faint) if self.band_1 == 'g' else (r < mag_faint)
            mag_obs = visual(g[cut] - self.distance_modulus, r[cut] - self.distance_modulus)
            abs_mag_obs_array[ii] = sumMag(mag_obs, mag_unobs)

        # ADW: This shouldn't be necessary
        #abs_mag_obs_array = np.sort(abs_mag_obs_array)[::-1]

        # ADW: Careful, fainter abs mag is larger (less negative) number
        q = [100*alpha/2., 50, 100*(1-alpha/2.)]
        hi,med,lo = np.percentile(abs_mag_obs_array,q)
        return ugali.utils.stats.interval(med,lo,hi)

    def simulate(self, stellar_mass, distance_modulus=None, **kwargs):
        """
        Simulate a set of stellar magnitudes (no uncertainty) for a satellite of a given stellar mass and distance.
        """
        if distance_modulus is None: distance_modulus = self.distance_modulus
        # Total number of stars in system
        n = int(stellar_mass / self.stellar_mass()) 
        f_1 = scipy.interpolate.interp1d(self.mass_init, self.mag_1)
        f_2 = scipy.interpolate.interp1d(self.mass_init, self.mag_2)
        mass_init_sample = self.imf.sample(n, np.min(self.mass_init), np.max(self.mass_init), **kwargs)
        mag_1_sample, mag_2_sample = f_1(mass_init_sample), f_2(mass_init_sample) 
        return mag_1_sample + distance_modulus, mag_2_sample + distance_modulus

    def observableFractionCMDX(self, mask, distance_modulus, mass_min=0.1):
        """
        Compute observable fraction of stars with masses greater than mass_min in each 
        pixel in the interior region of the mask.

        ADW: Careful, this function is fragile! The selection here should
             be the same as mask.restrictCatalogToObservable space. However,
             for technical reasons it is faster to do the calculation with
             broadcasting here.
        ADW: Could this function be even faster / more readable?
        ADW: Should this include magnitude error leakage?
        """
        mass_init_array,mass_pdf_array,mass_act_array,mag_1_array,mag_2_array = self.sample(mass_min=mass_min,full_data_range=False)
        mag = mag_1_array if self.band_1_detection else mag_2_array
        color = mag_1_array - mag_2_array

        # ADW: Only calculate observable fraction over interior pixels...
        pixels = mask.roi.pixels_interior
        mag_1_mask = mask.mask_1.mask_roi_sparse[mask.roi.pixel_interior_cut]
        mag_2_mask = mask.mask_2.mask_roi_sparse[mask.roi.pixel_interior_cut]

        # ADW: Restrict mag and color to range of mask with sufficient solid angle
        cmd_cut = ugali.utils.binning.take2D(mask.solid_angle_cmd,color,mag+distance_modulus,
                                             mask.roi.bins_color, mask.roi.bins_mag) > 0
        # Pre-apply these cuts to the 1D mass_pdf_array to save time
        mass_pdf_cut = mass_pdf_array*cmd_cut

        # Create 2D arrays of cuts for each pixel
        mask_1_cut = (mag_1_array+distance_modulus)[:,np.newaxis] < mag_1_mask
        mask_2_cut = (mag_2_array+distance_modulus)[:,np.newaxis] < mag_2_mask
        mask_cut_repeat = mask_1_cut & mask_2_cut

        observable_fraction = (mass_pdf_cut[:,np.newaxis]*mask_cut_repeat).sum(axis=0)
        return observable_fraction

    def observableFractionCMD(self, mask, distance_modulus, mass_min=0.1):
        """
        Compute observable fraction of stars with masses greater than mass_min in each 
        pixel in the interior region of the mask.

        ADW: Careful, this function is fragile! The selection here should
             be the same as mask.restrictCatalogToObservable space. However,
             for technical reasons it is faster to do the calculation with
             broadcasting here.
        ADW: Could this function be even faster / more readable?
        ADW: Should this include magnitude error leakage?
        """
        if distance_modulus is None: distance_modulus = self.distance_modulus
        mass_init,mass_pdf,mass_act,mag_1,mag_2 = self.sample(mass_min=mass_min,full_data_range=False)

        mag = mag_1 if self.band_1_detection else mag_2
        color = mag_1 - mag_2

        # ADW: Only calculate observable fraction for unique mask values
        mag_1_mask,mag_2_mask = mask.mask_roi_unique.T

        # ADW: Restrict mag and color to range of mask with sufficient solid angle
        cmd_cut = ugali.utils.binning.take2D(mask.solid_angle_cmd,color,mag+distance_modulus,
                                             mask.roi.bins_color, mask.roi.bins_mag) > 0
        # Pre-apply these cuts to the 1D mass_pdf_array to save time
        mass_pdf_cut = mass_pdf*cmd_cut

        # Create 2D arrays of cuts for each pixel
        mask_1_cut = (mag_1+distance_modulus)[:,np.newaxis] < mag_1_mask
        mask_2_cut = (mag_2+distance_modulus)[:,np.newaxis] < mag_2_mask
        mask_cut_repeat = (mask_1_cut & mask_2_cut)

        # Condense back into one per digi
        observable_fraction = (mass_pdf_cut[:,np.newaxis]*mask_cut_repeat).sum(axis=0)

        # Expand to the roi and multiply by coverage fraction
        return observable_fraction[mask.mask_roi_digi[mask.roi.pixel_interior_cut]] * mask.frac_interior_sparse


    def observableFractionCDF(self, mask, distance_modulus, mass_min=0.1):
        """
        Compute observable fraction of stars with masses greater than mass_min in each 
        pixel in the interior region of the mask. Incorporates simplistic
        photometric errors.

        ADW: Careful, this function is fragile! The selection here should
             be the same as mask.restrictCatalogToObservable space. However,
             for technical reasons it is faster to do the calculation with
             broadcasting here.
        ADW: This function is currently a rate-limiting step in the likelihood 
             calculation. Could it be faster?
        """
        method = 'step'

        mass_init,mass_pdf,mass_act,mag_1,mag_2 = self.sample(mass_min=mass_min,full_data_range=False)
         
        mag_1 = mag_1+distance_modulus
        mag_2 = mag_2+distance_modulus
         
        mask_1,mask_2 = mask.mask_roi_unique.T
         
        mag_err_1 = mask.photo_err_1(mask_1[:,np.newaxis]-mag_1)
        mag_err_2 = mask.photo_err_2(mask_2[:,np.newaxis]-mag_2)
         
        # "upper" bound set by maglim
        delta_hi_1 = (mask_1[:,np.newaxis]-mag_1)/mag_err_1
        delta_hi_2 = (mask_2[:,np.newaxis]-mag_2)/mag_err_2
         
        # "lower" bound set by bins_mag (maglim shouldn't be 0)
        delta_lo_1 = (mask.roi.bins_mag[0]-mag_1)/mag_err_1
        delta_lo_2 = (mask.roi.bins_mag[0]-mag_2)/mag_err_2
         
        cdf_1 = norm_cdf(delta_hi_1) - norm_cdf(delta_lo_1)
        cdf_2 = norm_cdf(delta_hi_2) - norm_cdf(delta_lo_2)
        cdf = cdf_1*cdf_2
         
        if method is None or method == 'none':
            comp_cdf = cdf
        elif self.band_1_detection == True:
            comp = mask.mask_1.completeness(mag_1, method=method)
            comp_cdf = comp*cdf
        elif self.band_1_detection == False:
            comp =mask.mask_2.completeness(mag_2, method=method)
            comp_cdf = comp*cdf
        else:
            comp_1 = mask.mask_1.completeness(mag_1, method=method)
            comp_2 = mask.mask_2.completeness(mag_2, method=method)
            comp_cdf = comp_1*comp_2*cdf
         
        observable_fraction = (mass_pdf[np.newaxis]*comp_cdf).sum(axis=-1)
        return observable_fraction[mask.mask_roi_digi[mask.roi.pixel_interior_cut]]

    def observableFractionMMD(self, mask, distance_modulus, mass_min=0.1):
        # This can be done faster...
        logger.info('Calculating observable fraction from MMD')

        mmd = self.signalMMD(mask,distance_modulus)
        obs_frac = mmd.sum(axis=-1).sum(axis=-1)[mask.mask_roi_digi[mask.roi.pixel_interior_cut]]
        return obs_frac

    observable_fraction = observableFractionCMD
    observableFraction = observable_fraction

    def signalMMD(self, mask, distance_modulus, mass_min=0.1, nsigma=5, delta_mag=0.03, mass_steps=1000, method='step'):
        roi = mask.roi
       
        mass_init,mass_pdf,mass_act,mag_1,mag_2 = self.sample(mass_steps=mass_steps,mass_min=mass_min,full_data_range=False)
        mag_1 = mag_1+distance_modulus
        mag_2 = mag_2+distance_modulus
         
        mask_1,mask_2 = mask.mask_roi_unique.T
         
        mag_err_1 = mask.photo_err_1(mask_1[:,np.newaxis]-mag_1)
        mag_err_2 = mask.photo_err_2(mask_2[:,np.newaxis]-mag_2)
         
        # Set mag_err for mask==0 to epsilon
        mag_err_1[mask_1==0] *= -np.inf
        mag_err_2[mask_2==0] *= -np.inf
         
        #edges_mag = np.arange(mask.roi.bins_mag[0] - (0.5*delta_mag),
        #                      mask.roi.bins_mag[-1] + (0.5*delta_mag),
        #                      delta_mag)
        #nedges = edges_mag.shape[0]

        nedges = np.rint((roi.bins_mag[-1]-roi.bins_mag[0])/delta_mag)+1
        edges_mag,delta_mag = np.linspace(roi.bins_mag[0],roi.bins_mag[-1],nedges,retstep=True)
        edges_mag_1 = edges_mag_2 = edges_mag
        nbins = nedges - 1
         
        mag_err_1_max = mag_err_1.max(axis=0)
        mag_err_2_max = mag_err_2.max(axis=0)
         
        max_idx_1 = np.searchsorted(edges_mag[:-1],mag_1+nsigma*mag_err_1_max)
        min_idx_1 = np.searchsorted(edges_mag[:-1],mag_1-nsigma*mag_err_1_max)
        max_idx_2 = np.searchsorted(edges_mag[:-1],mag_2+nsigma*mag_err_1_max)
        min_idx_2 = np.searchsorted(edges_mag[:-1],mag_2-nsigma*mag_err_1_max)
         
        # Select only isochrone values that will contribute to the MMD space
        sel = (max_idx_1>0)&(min_idx_1<nbins)&(max_idx_2>0)&(min_idx_2<nbins)
        if sel.sum() == 0:
            msg = 'No isochrone points in magnitude selection range'
            raise Exception(msg)
         
        mag_1,mag_2 = mag_1[sel],mag_2[sel]
        mag_err_1,mag_err_2 = mag_err_1[:,sel],mag_err_2[:,sel]
        mass_pdf = mass_pdf[sel]
        mag_err_1_max = mag_err_1.max(axis=0)
        mag_err_2_max = mag_err_2.max(axis=0)
        min_idx_1,max_idx_1 = min_idx_1[sel],max_idx_1[sel]
        min_idx_2,max_idx_2 = min_idx_2[sel],max_idx_2[sel]
         
        nmaglim,niso = mag_err_1.shape
         
        # Find valid indices in MMD space (can we avoid this loop?)
        nidx = ((max_idx_1-min_idx_1)*(max_idx_2-min_idx_2))
        mag_idx = np.arange(niso).repeat(nidx)
        bin_idx = np.zeros(nidx.sum(),dtype=int)
        ii = 0
        # ADW: Can we avoid this loop?
        for i in range(niso):
            x = np.ravel_multi_index(np.mgrid[min_idx_1[i]:max_idx_1[i],
                                              min_idx_2[i]:max_idx_2[i]],
                                     [nbins,nbins]).ravel()
            bin_idx[ii:ii+len(x)] = x
            ii += len(x)
         
        #idx = np.unique(idx)
        idx_1,idx_2 = np.unravel_index(bin_idx,[nbins,nbins])
         
        # Pre-compute the indexed arrays to save time at the cost of memory
        mag_1_idx,mag_2_idx = mag_1[mag_idx],mag_2[mag_idx]
        mag_err_1_idx,mag_err_2_idx = mag_err_1[:,mag_idx],mag_err_2[:,mag_idx]
        edges_mag_1_idx,edges_mag_2_idx = edges_mag[idx_1],edges_mag[idx_2]
         
        arg_mag_1_hi = (mag_1_idx - edges_mag_1_idx) / mag_err_1_idx
        arg_mag_1_lo = arg_mag_1_hi - delta_mag/mag_err_1_idx
        arg_mag_2_hi = (mag_2_idx - edges_mag_2_idx) / mag_err_2_idx
        arg_mag_2_lo = arg_mag_2_hi - delta_mag/mag_err_2_idx
         
        del mag_1_idx,mag_2_idx
        del mag_err_1_idx,mag_err_2_idx
        del edges_mag_1_idx,edges_mag_2_idx
         
        # This may become necessary with more maglim bins         
        ### # PDF is only ~nonzero for object-bin pairs within 5 sigma in both magnitudes  
        ### index_nonzero = np.nonzero((arg_mag_1_hi > -nsigma)*(arg_mag_1_lo < nsigma) \
        ###                                *(arg_mag_2_hi > -nsigma)*(arg_mag_2_lo < nsigma))
        ### idx_maglim,idx_iso,idx_idx = index_nonzero
        ### subidx = idx[idx_idx]
         
        pdf_val_1 = norm_cdf(arg_mag_1_hi)-norm_cdf(arg_mag_1_lo)
        pdf_val_2 = norm_cdf(arg_mag_2_hi)-norm_cdf(arg_mag_2_lo)
        pdf_val = pdf_val_1 * pdf_val_2
         
        # Deal with completeness
        if method is None or method == 'none':
            comp = None
        elif self.band_1_detection == True:
            comp=mask.completeness(mask_1[:,np.newaxis]-mag_1, method=method)
        elif self.band_1_detection == False:
            comp=mask.completeness(mask_2[:,np.newaxis]-mag_2, method=method)
        else:
            comp_1 = mask.completeness(mask_1[:,np.newaxis]-mag_1, method=method)
            comp_2 = mask.completeness(mask_2[:,np.newaxis]-mag_2, method=method)
            comp = comp_1*comp_2
         
        if comp is not None:
            comp_pdf_val = pdf_val*comp[:,mag_idx]
        else:
            comp_pdf_val = pdf_val
         
        # Deal with mass pdf values
        scaled_pdf_val = comp_pdf_val*mass_pdf[mag_idx]
         
        # Do the sum without creating the huge sparse array.
        label_idx = np.arange(nmaglim*nbins**2).reshape(nmaglim,nbins**2)
        labels = label_idx[:,bin_idx]
        sum_pdf = ndimage.sum(scaled_pdf_val,labels,label_idx.flat).reshape(nmaglim,nbins**2)
         
        # This is the clipping of the pdf at the maglim
        # Probably want to move this out of this function.
        final_pdf = sum_pdf.reshape(nmaglim,nbins,nbins)
         
        argmax_hi_1 = np.argmax((mask_1[:,np.newaxis] <= edges_mag[1:]),axis=1)
        argmax_hi_2 = np.argmax((mask_2[:,np.newaxis] <= edges_mag[1:]),axis=1)
         
        bin_frac_1 = (mask_1 - edges_mag[argmax_hi_1])/delta_mag
        bin_frac_2 = (mask_2 - edges_mag[argmax_hi_2])/delta_mag
         
        for i,(argmax_1,argmax_2) in enumerate(zip(argmax_hi_1,argmax_hi_2)):
            final_pdf[i,argmax_1,:] *= bin_frac_1[i]
            final_pdf[i,:,argmax_2] *= bin_frac_2[i]
            final_pdf[i,argmax_1+1:,:] = 0
            final_pdf[i,:,argmax_2+1:] = 0
         
        ## This is the actual data selection cut...
        #bins_2,bins_1 = np.meshgrid(edges_mag[:-1],edges_mag[:-1])
        #cut = (bins_1 < mask_1[:,np.newaxis,np.newaxis])*(bins_2 < mask_2[:,np.newaxis,np.newaxis])
        #final_pdf = sum_pdf.reshape(nmaglim,nbins,nbins)*cut
        return final_pdf

    def histogram2d(self,distance_modulus=None,delta_mag=0.03,steps=10000):
        """
        Return a 2D histogram the isochrone in mag-mag space.

        Parameters:
        -----------
        distance_modulus : distance modulus to calculate histogram at
        delta_mag : magnitude bin size
        mass_steps : number of steps to sample isochrone at

        Returns:
        --------
        bins_mag_1 : bin edges for first magnitude
        bins_mag_2 : bin edges for second magnitude
        isochrone_pdf : weighted pdf of isochrone in each bin
        """
        if distance_modulus is not None:
            self.distance_modulus = distance_modulus

        # Isochrone will be binned, so might as well sample lots of points
        mass_init,mass_pdf,mass_act,mag_1,mag_2 = self.sample(mass_steps=steps)

        #logger.warning("Fudging intrinisic dispersion in isochrone.")
        #mag_1 += np.random.normal(scale=0.02,size=len(mag_1))
        #mag_2 += np.random.normal(scale=0.02,size=len(mag_2))

        # We cast to np.float32 to save memory
        bins_mag_1 = np.arange(self.mod+mag_1.min() - (0.5*delta_mag),
                               self.mod+mag_1.max() + (0.5*delta_mag),
                               delta_mag).astype(np.float32)
        bins_mag_2 = np.arange(self.mod+mag_2.min() - (0.5*delta_mag),
                               self.mod+mag_2.max() + (0.5*delta_mag),
                               delta_mag).astype(np.float32)
 
        # ADW: Completeness needs to go in mass_pdf here...
        isochrone_pdf = np.histogram2d(self.mod + mag_1,
                                       self.mod + mag_2,
                                       bins=[bins_mag_1, bins_mag_2],
                                       weights=mass_pdf)[0].astype(np.float32)
 
        return isochrone_pdf, bins_mag_1, bins_mag_2
 
    def pdf_mmd(self, lon, lat, mag_1, mag_2, distance_modulus, mask, delta_mag=0.03, steps=1000):
        """
        Ok, now here comes the beauty of having the signal MMD.
        """
        logger.info('Running MMD pdf')
 
        roi = mask.roi
        mmd = self.signalMMD(mask,distance_modulus,delta_mag=delta_mag,mass_steps=steps)
        
        # This is fragile, store this information somewhere else...
        nedges = np.rint((roi.bins_mag[-1]-roi.bins_mag[0])/delta_mag)+1
        edges_mag,delta_mag = np.linspace(roi.bins_mag[0],roi.bins_mag[-1],nedges,retstep=True)
                                    
        idx_mag_1 = np.searchsorted(edges_mag,mag_1)
        idx_mag_2 = np.searchsorted(edges_mag,mag_2)
 
        if np.any(idx_mag_1 > nedges) or np.any(idx_mag_1 == 0):
            msg = "Magnitude out of range..."
            raise Exception(msg)
        if np.any(idx_mag_2 > nedges) or np.any(idx_mag_2 == 0):
            msg = "Magnitude out of range..."
            raise Exception(msg)
 
        idx = mask.roi.indexROI(lon,lat)
        u_color = mmd[(mask.mask_roi_digi[idx],idx_mag_1,idx_mag_2)]
 
        # Remove the bin size to convert the pdf to units of mag^-2
        u_color /= delta_mag**2
 
        return u_color

    #import memory_profiler
    #@memory_profiler.profile
    def pdf(self, mag_1, mag_2, mag_err_1, mag_err_2, 
            distance_modulus=None, delta_mag=0.03, steps=10000):
        """
        Compute isochrone probability for each catalog object.
 
        ADW: This is a memory intensive function, so try as much as
        possible to keep array types at `float32` or smaller (maybe
        using add.at would be good?)
        ADW: Still a little speed to be gained here (broadcasting)
        ADW: Units? [mag^-2] [per sr?]

        Parameters:
        -----------
        mag_1 : magnitude of stars (pdf sample points) in first band
        mag_2 : magnitude of stars (pdf sample points) in second band
        mag_err_1 : magnitude error of stars (pdf sample points) in first band
        mag_err_2 : magnitude error of stars (pdf sample points) in second band
        distance_modulus : distance modulus of isochrone
        delta_mag : magnitude binning for evaluating the pdf
        steps : number of isochrone sample points

        Returns:
        --------
        u_color : probability that the star belongs to the isochrone [mag^-2]
        """
        nsigma = 5.0
        #pad = 1. # mag

        if distance_modulus is None: 
            distance_modulus = self.distance_modulus

        # ADW: HACK TO ADD SYSTEMATIC UNCERTAINTY (0.010 mag)
        mag_err_1 = np.sqrt(mag_err_1**2 + 0.01**2)
        mag_err_2 = np.sqrt(mag_err_2**2 + 0.01**2)
 
        # Binned pdf of the isochrone
        histo_pdf,bins_mag_1,bins_mag_2 = self.histogram2d(distance_modulus,delta_mag,steps)
         
        # Keep only isochrone bins that are within the magnitude
        # space of the sample
        mag_1_mesh, mag_2_mesh = np.meshgrid(bins_mag_2[1:], bins_mag_1[1:])
         
        # pdf contribution only calculated out to nsigma,
        # so padding shouldn't be necessary.
        mag_1_max = np.max(mag_1+nsigma*mag_err_1)# +pad 
        mag_1_min = np.min(mag_1-nsigma*mag_err_1)# -pad 
        mag_2_max = np.max(mag_2+nsigma*mag_err_2)# +pad 
        mag_2_min = np.min(mag_2-nsigma*mag_err_2)# -pad 
         
        in_mag_space = ((mag_1_mesh>=mag_1_min)&(mag_1_mesh<=mag_1_max))
        in_mag_space*= ((mag_2_mesh>=mag_2_min)&(mag_2_mesh<=mag_2_max))
        histo_pdf *= in_mag_space
 
        idx_mag_1, idx_mag_2 = np.nonzero(histo_pdf)
        isochrone_pdf = histo_pdf[idx_mag_1, idx_mag_2]
 
        n_catalog = len(mag_1)
        n_isochrone_bins = len(idx_mag_1)

        mag_1 = mag_1.reshape([n_catalog, 1])
        mag_err_1 = mag_err_1.reshape([n_catalog, 1])
        mag_2 = mag_2.reshape([n_catalog, 1])
        mag_err_2 = mag_err_2.reshape([n_catalog, 1])

        # Calculate (normalized) distance between each catalog object
        # and isochrone bin. Assume normally distributed photometric
        # uncertainties so that the normalized distance is:
        #   norm_dist = (mag_1 - bins_mag_1)/mag_err_1

        # ADW: Creating the dist arrays is memory intensive.
        # Can we cut it down (maybe with add.at)?
        dist_mag_1_hi = (mag_1-bins_mag_1[idx_mag_1])/mag_err_1
        dist_mag_1_lo = (mag_1-bins_mag_1[idx_mag_1+1])/mag_err_1

        dist_mag_2_hi = (mag_2-bins_mag_2[idx_mag_2])/mag_err_2
        dist_mag_2_lo = (mag_2-bins_mag_2[idx_mag_2+1])/mag_err_2
         
        # Only calculate the PDF using bins that are < nsigma from the
        # data point (i.e., where it is ~nonzero).
        idx_nonzero_0,idx_nonzero_1 = np.nonzero((dist_mag_1_hi > -nsigma) \
                                                *(dist_mag_1_lo < nsigma)\
                                                *(dist_mag_2_hi > -nsigma)\
                                                *(dist_mag_2_lo < nsigma))

        # Now calculate the pdf as the delta of the normalized cdf
        # (more accurate than the point evaluation of the pdf)
        pdf_mag_1 = np.zeros([n_catalog, n_isochrone_bins],dtype=np.float32)
        pdf_mag_1[idx_nonzero_0,idx_nonzero_1] = norm_cdf(dist_mag_1_hi[idx_nonzero_0,idx_nonzero_1]) \
            - norm_cdf(dist_mag_1_lo[idx_nonzero_0,idx_nonzero_1])

        pdf_mag_2 = np.zeros([n_catalog, n_isochrone_bins],dtype=np.float32)
        pdf_mag_2[idx_nonzero_0,idx_nonzero_1] = norm_cdf(dist_mag_2_hi[idx_nonzero_0,idx_nonzero_1]) \
            - norm_cdf(dist_mag_2_lo[idx_nonzero_0,idx_nonzero_1])

        # Signal "color probability" (as opposed to "spatial
        # probability", but more accurately "isochrone probability")
        # is the product of PDFs for each object-bin pair summed over
        # isochrone bins 

        #ADW: Here is where add.at would be good...
        u_color = np.sum(pdf_mag_1 * pdf_mag_2 * isochrone_pdf, axis=1)
 
        # Remove the bin size to convert the pdf to units of mag^-2
        u_color /= delta_mag**2

        return u_color.astype(np.float32)
    
 
    def raw_separation(self,mag_1,mag_2,steps=10000):
        """ 
        Calculate the separation in magnitude-magnitude space between points and isochrone. Uses a dense sampling of the isochrone and calculates the metric distance from any isochrone sample point.

        Parameters:
        -----------
        mag_1 : The magnitude of the test points in the first band
        mag_2 : The magnitude of the test points in the second band
        steps : Number of steps to sample the isochrone

        Returns:
        --------
        sep : Minimum separation between test points and isochrone sample
        """
     
        # http://stackoverflow.com/q/12653120/
        mag_1 = np.array(mag_1,copy=False,ndmin=1)
        mag_2 = np.array(mag_2,copy=False,ndmin=1)
     
        init,pdf,act,iso_mag_1,iso_mag_2 = self.sample(mass_steps=steps)
        iso_mag_1+=self.distance_modulus
        iso_mag_2+=self.distance_modulus
     
        iso_cut = (iso_mag_1<np.max(mag_1))&(iso_mag_1>np.min(mag_1)) | \
                  (iso_mag_2<np.max(mag_2))&(iso_mag_2>np.min(mag_2))
        iso_mag_1 = iso_mag_1[iso_cut]
        iso_mag_2 = iso_mag_2[iso_cut]
         
        dist_mag_1 = mag_1[:,np.newaxis]-iso_mag_1
        dist_mag_2 = mag_2[:,np.newaxis]-iso_mag_2
        
        return np.min(np.sqrt(dist_mag_1**2 + dist_mag_2**2),axis=1)


    def separation(self, mag_1, mag_2):
        """ 
        Calculate the separation between a specific point and the
        isochrone in magnitude-magnitude space. Uses an interpolation

        ADW: Could speed this up...

        Parameters:
        -----------
        mag_1 : The magnitude of the test points in the first band
        mag_2 : The magnitude of the test points in the second band

        Returns:
        --------
        sep : Minimum separation between test points and isochrone interpolation
        """

        iso_mag_1 = self.mag_1 + self.distance_modulus
        iso_mag_2 = self.mag_2 + self.distance_modulus
        
        def interp_iso(iso_mag_1,iso_mag_2,mag_1,mag_2):
            interp_1 = scipy.interpolate.interp1d(iso_mag_1,iso_mag_2,bounds_error=False)
            interp_2 = scipy.interpolate.interp1d(iso_mag_2,iso_mag_1,bounds_error=False)

            dy = interp_1(mag_1) - mag_2
            dx = interp_2(mag_2) - mag_1

            dmag_1 = np.fabs(dx*dy) / (dx**2 + dy**2) * dy
            dmag_2 = np.fabs(dx*dy) / (dx**2 + dy**2) * dx

            return dmag_1, dmag_2

        # Separate the various stellar evolution stages
        if np.issubdtype(self.stage.dtype,np.number):
            sel = (self.stage < self.hb_stage)
        else:
            sel = (self.stage != self.hb_stage)

        # First do the MS/RGB
        rgb_mag_1 = iso_mag_1[sel]
        rgb_mag_2 = iso_mag_2[sel]
        dmag_1,dmag_2 = interp_iso(rgb_mag_1,rgb_mag_2,mag_1,mag_2)

        # Then do the HB (if it exists)
        if not np.all(sel):
            hb_mag_1 = iso_mag_1[~sel]
            hb_mag_2 = iso_mag_2[~sel]

            hb_dmag_1,hb_dmag_2 = interp_iso(hb_mag_1,hb_mag_2,mag_1,mag_2)

            dmag_1 = np.nanmin([dmag_1,hb_dmag_1],axis=0)
            dmag_2 = np.nanmin([dmag_2,hb_dmag_2],axis=0)

        #return dmag_1,dmag_2
        return np.sqrt(dmag_1**2 + dmag_2**2)
コード例 #10
0
class CompositeIsochrone(IsochroneModel):
    _params = odict([
        ('distance_modulus', Parameter(15.0, [10.0, 30.0]) ),
    ])
    _mapping = odict([
            ('mod','distance_modulus'),
            ('a','age'),                 
            ('z','metallicity'),
            ])

    defaults = (IsochroneModel.defaults) + (
        ('type','Bressan2012','Default type of isochrone to create'),
        ('weights',None,'Relative weights for each isochrone'),
        )
    
    def __init__(self, isochrones, **kwargs):
        super(CompositeIsochrone,self).__init__(**kwargs)

        # Remove composite kwargs so that others can be used as defaults
        kwargs.pop('type',None)
        kwargs.pop('weights',None)

        self.isochrones = []
        for i in isochrones:
            if isinstance(i,Isochrone):
                iso = i
            else:
                # Set the defaults from composite
                [i.setdefault(*kw) for kw in kwargs.items()]
                # Default isochrone type (ADW: do we want this?)
                name = i.pop('name',self.type)
                #iso = isochroneFactory(name=name,**i)
                iso = factory(name=name,**i)
            # Tie the distance modulus
            iso.params['distance_modulus'] = self.params['distance_modulus']
            self.isochrones.append(iso)
        
        if self.weights is None: self.weights = np.ones(len(self.isochrones))
        self.weights /= np.sum(np.asarray(self.weights))
        self.weights = self.weights.tolist()

        if len(self.isochrones) != len(self.weights):
            msg = 'Length of isochrone and weight arrays must be equal'
            raise ValueError(msg)

    def __getitem__(self, key):
        return self.isochrones[key]
 
    def __str__(self,indent=0):
        ret = super(CompositeIsochrone,self).__str__(indent)
        ret += '\n{0:>{2}}{1}'.format('','Isochrones:',indent+2)
        for i in self:
            ret += '\n{0}'.format(i.__str__(indent+4))
        return ret

    @property
    def age(self):
        return np.array([i.age for i in self])

    @property
    def metallicity(self):
        return np.array([i.metallicity for i in self])

    def composite_decorator(func):
        """
        Decorator for wrapping functions that calculate a weighted sum
        """
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            total = []
            for weight,iso in zip(self.weights,self.isochrones):
                subfunc = getattr(iso,func.__name__)
                total.append(weight*subfunc(*args,**kwargs))
            return np.sum(total,axis=0)
        return wrapper

    def sample(self, **kwargs):
        samples = [iso.sample(**kwargs) for iso in self.isochrones]
        for weight,sample in zip(self.weights,samples):
            sample[1] *= weight

        return np.hstack(samples)

    def separation(self, *args, **kwargs):
        separations = [iso.separation(*args,**kwargs) for iso in self.isochrones]
        return np.nanmin(separations,axis=0)
    
    def todict(self):
        ret = super(CompositeIsochrone,self).todict()
        ret['isochrones'] = [iso.todict() for iso in self.isochrones]
        return ret

    @composite_decorator
    def stellar_mass(self, *args, **kwargs): pass

    @composite_decorator
    def stellar_luminosity(self, *args, **kwargs): pass

    @composite_decorator
    def observable_fraction(self, *args, **kwargs): pass

    @composite_decorator
    def observableFractionX(self, *args, **kwargs): pass

    @composite_decorator
    def signalMMD(self, *args, **kwargs): pass

    # ADW: For temporary backwards compatibility
    stellarMass = stellar_mass
    stellarLuminosity = stellar_luminosity
    observableFraction = observable_fraction

    def absolute_magnitude(self, richness=1, steps=1e4):
        raise ValueError("Not implemented for CompositeIsochrone")

    def absolute_magnitude_martin(self, richness=1, steps=1e4):
        raise ValueError("Not implemented for CompositeIsochrone")
コード例 #11
0
ファイル: kernel.py プロジェクト: kadrlica/ugali
class Kernel(Model):
    """
    Base class for kernels.
    """
    _params = odict([
        ('lon', Parameter(0.0, [0.0, 360.])),
        ('lat', Parameter(0.0, [-90., 90.])),
    ])
    _mapping = odict([])
    _proj = 'ait'

    def __init__(self, proj='ait', **kwargs):
        # This __init__ is probably not necessary...
        self.proj = proj
        super(Kernel, self).__init__(**kwargs)

    def __call__(self, lon, lat):
        """
        Return the value of the pdf at a give location.

        Parameters
        ----------
        lon : longitude (deg)
        lat : latitude (deg)
        
        Returns
        -------
        pdf : normalized, truncated pdf
        """
        return self.pdf(lon, lat)

    @abstractmethod
    def _kernel(self, r):
        """Unnormalized, untruncated kernel"""
        pass

    def _pdf(self, radius):
        """Unnormalized, truncated kernel"""
        return np.where(radius <= self.edge, self._kernel(radius), 0.)

    @abstractmethod
    def pdf(self, lon, lat):
        """Normalized, truncated pdf"""
        pass

    @property
    def norm(self):
        """Normalization from the integated pdf"""
        return 1. / self.integrate()

    @property
    def projector(self):
        """Projector used to transform to local sky coordinates."""
        if self.proj is None or self.proj.lower() == 'none':
            return None
        else:
            return Projector(self.lon, self.lat, self.proj)

    def integrate(self, rmin=0, rmax=np.inf):
        """
        Calculate the 2D integral of the 1D surface brightness profile 
        (i.e, the flux) between rmin and rmax (elliptical radii). 

        Parameters:
        -----------
        rmin : minimum integration radius (deg)
        rmax : maximum integration radius (deg)

        Returns:
        --------
        integral : Solid angle integral (deg^2)
        """
        if rmin < 0: raise Exception('rmin must be >= 0')
        integrand = lambda r: self._pdf(r) * 2 * np.pi * r
        return scipy.integrate.quad(integrand,
                                    rmin,
                                    rmax,
                                    full_output=True,
                                    epsabs=0)[0]