Simulating Color-Magnitude Diagrams
Modelling observations of resolved stellar populations (e.g., color-magnitude or Hess diagrams) with user-defined star formation histories can be useful for comparison to actual observations, but also enables a number of other scientific activities (e.g., making predictions to motivate observational proposals). To support these uses we offer methods for sampling stellar populations from isochrones using user-defined star formation histories, initial mass functions, and stellar binary models. These methods require data from user-provided isochrones (this package does not provide any), an initial mass function model (such as those provided in InitialMassFunctions.jl), and a model specifying how (or if) to sample binary or multi-star systems.
The simplest methods only sample stars from a single stellar population. We provide a method that samples up to a provided stellar mass, generate_stars_mass
(e.g., $10^7 \, \text{M}_\odot$) and a method that samples up to a provided absolute magnitude generate_stars_mag
(e.g., $M_V=-10$). These are documented under the first subsection below. These methods are single-threaded.
We also offer methods for sampling populations with complex star formation histories; these are implicitly multi-threaded across the separate populations if you start Julia with multiple threads (e.g., with julia -t 4
or similar). We provide generate_stars_mass_composite
for sampling such populations up to a provided stellar mass and generate_stars_mag_composite
for sampling such populations up to a provided absolute magnitude. These are documented under the second subsection below.
Simple Stellar Populations
StarFormationHistories.generate_stars_mass
— Functiongenerate_stars_mass(mini_vec::AbstractVector{<:Number},
mags, mag_names::AbstractVector{String},
limit::Number,
imf::Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous};
dist_mod::Number=0,
rng::Random.AbstractRNG=Random.default_rng(),
mag_lim::Number = Inf,
mag_lim_name::String = "V",
binary_model::StarFormationHistories.AbstractBinaryModel =
StarFormationHistories.RandomBinaryPairs(0.3))
Generates a random sample of stars from an isochrone defined by the provided initial stellar masses mini_vec
, absolute magnitudes mags
, and filter names mag_names
with total population birth stellar mass limit
(e.g., 1e5 solar masses). Initial stellar masses are sampled from the provided imf
.
Arguments
mini_vec::AbstractVector{<:Number}
contains the initial masses (in solar masses) for the stars in the isochrone; must be mutable as we callInterpolations.deduplicate_knots!(mini_vec)
.mags
contains the absolute magnitudes from the isochrone in the desired filters corresponding to the same stars as provided inmini_vec
.mags
is internally interpreted and converted into a standard format byStarFormationHistories.ingest_mags
. Valid inputs are:mags::AbstractVector{AbstractVector{<:Number}}
, in which case the length of the outer vectorlength(mags)
can either be equal tolength(mini_vec)
, in which case the length of the inner vectors must all be equal to the number of filters you are providing, or the length of the outer vector can be equal to the number of filters you are providing, and the length of the inner vectors must all be equal tolength(mini_vec)
; this is the more common use-case.mags::AbstractMatrix{<:Number}
, in which casemags
must be 2-dimensional. Valid shapes aresize(mags) == (length(mini_vec), nfilters)
orsize(mags) == (nfilters, length(mini_vec))
, withnfilters
being the number of filters you are providing.
mag_names::AbstractVector{String}
contains strings describing the filters you are providing inmags
; an example might be["B","V"]
. These are used whenmag_lim
is finite to determine what filter you want to use to limit the faintest stars you want returned.limit::Number
gives the total birth stellar mass of the population you want to sample. See the "Notes" section on population masses for more information.imf::Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous}
is a sampleable continuous univariate distribution implementing a stellar initial mass function with a definedrand(rng::Random.AbstractRNG, imf)
method to use for sampling masses. All instances ofDistributions.ContinuousUnivariateDistribution
are also valid. Implementations of commonly used IMFs are available in InitialMassFunctions.jl.
Keyword Arguments
dist_mod::Number=0
is the distance modulus (seeStarFormationHistories.distance_modulus
) you wish to have added to the returned magnitudes to simulate a population at a particular distance.rng::Random.AbstractRNG=Random.default_rng()
is the rng instance that will be used to sample the stellar initial masses fromimf
.mag_lim::Number=Inf
gives the faintest apparent magnitude for stars you want to be returned in the output. Stars fainter than this magnitude will still be sampled and contribute properly to the total mass of the population, but they will not be returned.mag_lim_name::String="V"
gives the filter name (as contained inmag_names
) to use when considering if a star is fainter thanmag_lim
. This is unused ifmag_lim
is infinite.binary_model::StarFormationHistories.AbstractBinaryModel=StarFormationHistories.RandomBinaryPairs(0.3)
is an instance of a model for treating binaries; currently provided options areNoBinaries
,RandomBinaryPairs
, andBinaryMassRatio
.
Returns
(sampled_masses, sampled_mags)
defined as
sampled_masses::Vector{SVector{N,eltype(imf)}}
is a vector containing the initial stellar masses of the stars sampled bysample_system
; see that method's documentation for details on format. In short, eachStaticArrays.SVector
represents one stellar system and each entry in eachStaticArrays.SVector
is one star in that system. Entries will be 0 when companions could have been sampled but were not (i.e., when using abinary_model
that supports multi-star systems).sampled_mags::Vector{SVector{N,<:Number}}
is a vector containingStaticArrays.SVectors
with the multi-band magnitudes of the sampled stars. To get the magnitude of stari
in bandj
, you index assampled_mags[i][j]
. This can be reinterpreted as a 2-dimensionalMatrix
withreduce(hcat,sampled_mags)
.
Notes
Population Masses
Given a particular isochrone with an initial mass vector mini_vec
, it will never cover the full range of stellar birth masses because stars that die before present-day are not included in the isochrone. However, these stars were born, and so contribute to the total birth mass of the system. There are two ways to properly account for this lost mass when sampling:
- Set the upper limit for masses that can be sampled from the
imf
distribution to a physical value for the maximum birth mass of stars (e.g., 100 solar masses). In this case, these stars will be sampled fromimf
, and will contribute their masses to the system, but they will not be returned if their birth mass is greater thanmaximum(mini_vec)
. This is typically easiest for the user and only results in ∼15% loss of efficiency for 10 Gyr isochrones. This approach is preferred when sampling with binaries. - Set the upper limit for masses that can be sampled from the
imf
distribution tomaximum(mini_vec)
and adjustlimit
to respect the amount of initial stellar mass lost by not sampling higher mass stars. This can be calculated asnew_limit = limit * ( QuadGK.quadgk(x->x*pdf(imf,x), minimum(mini_vec), maximum(mini_vec))[1] / QuadGK.quadgk(x->x*pdf(imf,x), minimum(imf), maximum(imf))[1] )
, with the multiplicative factor being the fraction of the population stellar mass contained in stars with initial masses betweenminimum(mini_vec)
andmaximum(mini_vec)
; this factor is the ratio
\[\frac{\int_a^b \ m \times \frac{dN(m)}{dm} \ dm}{\int_0^∞ \ m \times \frac{dN(m)}{dm} \ dm}.\]
Note that, if binaries are included, this approach only forms binary pairs between stars whose masses are less than maximum(mini_vec)
. This is probably not desired, so we recommend the previous approach when including binaries.
StarFormationHistories.generate_stars_mag
— Function(sampled_masses, sampled_mags) = generate_stars_mag(mini_vec::AbstractVector{<:Number}, mags, mag_names::AbstractVector{String}, absmag::Real, absmag_name::String, imf::Distributions.Sampleable{Distributions.Univariate,Distributions.Continuous}; dist_mod::Number=0, rng::AbstractRNG=default_rng(), mag_lim::Number=Inf, mag_lim_name::String="V", binary_model::StarFormationHistories.AbstractBinaryModel=RandomBinaryPairs(0.3))
Generates a mock stellar population from an isochrone defined by the provided initial stellar masses mini_vec
, absolute magnitudes mags
, and filter names mag_names
. The population is sampled to a total absolute magnitude absmag::Real
(e.g., -7 or -12) in the filter absmag_name::String
(e.g., "V" or "F606W") which is contained in the provided mag_names::AbstractVector{String}
. Other arguments are shared with generate_stars_mass
, which contains the main documentation.
Notes
Population Magnitudes
Unlike when sampling a population to a fixed initial birth mass, as is implemented in generate_stars_mass
, when generating a population up to a fixed absolute magnitude, only stars that survive to present-day contribute to the flux of the population. If you choose to limit the apparent magnitude of stars returned by passing the mag_lim
and mag_lim_name
keyword arguments, stars fainter than your chosen limit will still be sampled and will still contribute their luminosity to the total population, but they will not be contained in the returned output.
Complex Stellar Populations
StarFormationHistories.generate_stars_mass_composite
— Function(sampled_masses, sampled_mags) = generate_stars_mass_composite(mini_vec::AbstractVector{<:AbstractVector{<:Number}}, mags::AbstractVector, mag_names::AbstractVector{String}, limit::Number, massfrac::AbstractVector{<:Number}, imf::Sampleable{Univariate,Continuous}; kws...)
Generates a random sample of stars with a complex star formation history using multiple isochrones. Very similar to generate_stars_mass
except the isochrone-related arguments mini_vec
and mags
should now be vectors of vectors containing the relevant data for the full set of isochrones to be considered. The total birth stellar mass of the sampled population is given by limit
. The proportion of this mass allotted to each of the individual isochrones is given by the entries of the massfrac
vector. This basically just proportions limit
according to massfrac
and calls generate_stars_mass
for each of the individual stellar populations; as such it is set up to multi-thread across the multiple stellar populations.
Arguments
mini_vec::AbstractVector{<:AbstractVector{<:Number}}
contains the initial masses (in solar masses) for the stars in each isochrone; the internal vectors must be mutable as we will callInterpolations.deduplicate_knots!
on each. The length ofmini_vec
should be equal to the number of isochrones.mags
contains the absolute magnitudes from the isochrones in the desired filters corresponding to the same stars as provided inmini_vec
. The length ofmags
should be equal to the number of isochrones. The individual elements ofmags
are each internally interpreted and converted into a standard format byStarFormationHistories.ingest_mags
. The valid formats for the individual elements ofmags
are:AbstractVector{AbstractVector{<:Number}}
, in which case the length of the vectorlength(mags[i])
can either be equal tolength(mini_vec[i])
, in which case the length of the inner vectors must all be equal to the number of filters you are providing, or the length of the outer vector can be equal to the number of filters you are providing, and the length of the inner vectors must all be equal tolength(mini_vec[i])
; this is the more common use-case.AbstractMatrix{<:Number}
, in which casemags[i]
must be 2-dimensional. Valid shapes aresize(mags[i]) == (length(mini_vec[i]), nfilters)
orsize(mags[i]) == (nfilters, length(mini_vec[i]))
, withnfilters
being the number of filters you are providing.
mag_names::AbstractVector{String}
contains strings describing the filters you are providing inmags
; an example might be["B","V"]
. These are used whenmag_lim
is finite to determine what filter you want to use to limit the faintest stars you want returned. These are assumed to be the same for all isochrones.limit::Number
gives the total birth stellar mass of the population you want to sample.massfrac::AbstractVector{<:Number}
is vector giving the relative fraction of mass allotted to each individual stellar population; length must be equal to the length ofmini_vec
andmags
.imf::Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous}
is a sampleable continuous univariate distribution implementing a stellar initial mass function with a definedrand(rng::Random.AbstractRNG, imf)
method to use for sampling masses. All instances ofDistributions.ContinuousUnivariateDistribution
are also valid. Implementations of commonly used IMFs are available in InitialMassFunctions.jl.
Keyword Arguments
All keyword arguments kws...
are passed to generate_stars_mass
; you should refer to that method's documentation for more information.
Returns
sampled_masses::Vector{Vector{SVector{N,eltype(imf)}}}
is a vector of vectors containing the initial stellar masses of the sampled stars. The outer vectors are separated by the isochrone the stars were generated from; i.e., all ofsampled_masses[1]
were sampled frommini_vec[1]
and so on. These can be concatenated into a single vector withreduce(vcat,sampled_masses)
. The format of the containedStaticArrays.SVector
s are as output fromsample_system
; see that method's documentation for more details.sampled_mags::Vector{Vector{SVector{N,<:Number}}}
is a vector of vectors containingStaticArrays.SVectors
with the multi-band magnitudes of the sampled stars. The outer vectors are separated by the isochrone the stars were generated from; i.e. all ofsampled_mags[1]
were sampled frommags[1]
and so on. To get the magnitude of stari
in bandj
sampled from isochronek
, you would dosampled_mags[k][i][j]
. This can be concatenated into aVector{SVector}
withreduce(vcat,sampled_mags)
and a 2-DMatrix
withreduce(hcat,reduce(vcat,sampled_mags))
.
StarFormationHistories.generate_stars_mag_composite
— Function(sampled_masses, sampled_mags) = generate_stars_mag_composite(mini_vec::AbstractVector{<:AbstractVector{<:Number}}, mags::AbstractVector, mag_names::AbstractVector{String}, absmag::Number, absmag_name::String, fracs::AbstractVector{<:Number}, imf::Sampleable{Univariate,Continuous}; frac_type::String="lum", kws...)
Generates a random sample of stars with a complex star formation history using multiple isochrones. Very similar to generate_stars_mag
except the isochrone-related arguments mini_vec
and mags
should now be vectors of vectors containing the relevant data for the full set of isochrones to be considered. The total absolute magnitude of the sampled population is given by absmag
. The proportion of the luminosity allotted to each of the individual isochrones is given by the entries of the frac
vector. This basically just proportions the luminosity according to frac
and calls generate_stars_mag
for each of the individual stellar populations; as such it is set up to multi-thread across the multiple stellar populations.
Arguments
mini_vec::AbstractVector{<:AbstractVector{<:Number}}
contains the initial masses (in solar masses) for the stars in each isochrone; the internal vectors must be mutable as we will callInterpolations.deduplicate_knots!
on each. The length ofmini_vec
should be equal to the number of isochrones.mags
contains the absolute magnitudes from the isochrones in the desired filters corresponding to the same stars as provided inmini_vec
. The length ofmags
should be equal to the number of isochrones. The individual elements ofmags
are each internally interpreted and converted into a standard format byStarFormationHistories.ingest_mags
. The valid formats for the individual elements ofmags
are:AbstractVector{AbstractVector{<:Number}}
, in which case the length of the vectorlength(mags[i])
can either be equal tolength(mini_vec[i])
, in which case the length of the inner vectors must all be equal to the number of filters you are providing, or the length of the outer vector can be equal to the number of filters you are providing, and the length of the inner vectors must all be equal tolength(mini_vec[i])
; this is the more common use-case.AbstractMatrix{<:Number}
, in which casemags[i]
must be 2-dimensional. Valid shapes aresize(mags[i]) == (length(mini_vec[i]), nfilters)
orsize(mags[i]) == (nfilters, length(mini_vec[i]))
, withnfilters
being the number of filters you are providing.
mag_names::AbstractVector{String}
contains strings describing the filters you are providing inmags
; an example might be["B","V"]
. These are used whenmag_lim
is finite to determine what filter you want to use to limit the faintest stars you want returned. These are assumed to be the same for all isochrones.absmag::Number
gives the total absolute magnitude of the complex population to be sampled.fracs::AbstractVector{<:Number}
is a vector giving the relative fraction of luminosity or mass (determined by thefrac_type
keyword argument) allotted to each individual stellar population; length must be equal to the length ofmini_vec
andmags
.imf::Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous}
is a sampleable continuous univariate distribution implementing a stellar initial mass function with a definedrand(rng::Random.AbstractRNG, imf)
method to use for sampling masses. All instances ofDistributions.ContinuousUnivariateDistribution
are also valid. Implementations of commonly used IMFs are available in InitialMassFunctions.jl.
Keyword Arguments
frac_type::String
either "lum", in which casefracs
is assumed to contain the relative luminosity fractions for each individual isochrone, or "mass", in which case it is assumed thatfracs
contains mass fractions ("mass" is not yet implemented).
All other keyword arguments kws...
are passed to generate_stars_mag
; you should refer to that method's documentation for more information.
Returns
sampled_masses::Vector{Vector{SVector{N,eltype(imf)}}}
is a vector of vectors containing the initial stellar masses of the sampled stars. The outer vectors are separated by the isochrone the stars were generated from; i.e., all ofsampled_masses[1]
were sampled frommini_vec[1]
and so on. These can be concatenated into a single vector withreduce(vcat,sampled_masses)
. The format of the containedStaticArrays.SVector
s are as output fromsample_system
; see that method's documentation for more details.sampled_mags::Vector{Vector{SVector{N,<:Number}}}
is a vector of vectors containingStaticArrays.SVectors
with the multi-band magnitudes of the sampled stars. The outer vectors are separated by the isochrone the stars were generated from; i.e. all ofsampled_mags[1]
were sampled frommags[1]
and so on. To get the magnitude of stari
in bandj
sampled from isochronek
, you would dosampled_mags[k][i][j]
. This can be concatenated into aVector{SVector}
withreduce(vcat,sampled_mags)
and a 2-DMatrix
withreduce(hcat,reduce(vcat,sampled_mags))
.
Observational Effects
The output produced from the above methods are clean in the sense that they do not include any observational effects like photometric error or incompleteness. These effects should be implemented in a post-processing step. We provide a simple method model_cmd
that accepts user-defined photometric error and completeness functions and applies them to the initial catalog, returning a Monte Carlo realization of a possible observed catalog. This method assumes Gaussian photometric errors and that the photometric error and completeness functions are separable by filter – these assumptions are not applicable for all types of data, but the source code for the method is exceedingly simple (~20 lines) and should provide an example for how you could write a similar method that more accurately reflects your data.
StarFormationHistories.model_cmd
— Functionnew_mags [, good_idxs] = model_cmd(mags::AbstractVector{<:AbstractVector{<:Number}}, errfuncs, completefuncs; rng::Random.AbstractRNG=Random.default_rng(), ret_idxs::Bool=false)
Simple method for modelling photometric error and incompleteness to "mock observe" a pure catalog of stellar photometry, such as those produced by generate_stars_mass
and generate_stars_mag
. This method assumes Gaussian photometric errors and that the photometric error and completeness functions are separable by filter.
Arguments
mags::AbstractVector{<:AbstractVector{<:Number}}
: a vector of vectors giving the magnitudes of each star to be modelled. The first index is the per-star index and the second index is the per-filter index (somags[10][2]
would give the magnitude of the tenth star in the second filter). This is the same format as the magnitudes returned bygenerate_stars_mass
andgenerate_stars_mag
; to use output from the composite versions, you must firstreduce(vcat,mags)
before passing to this function.errfuncs
: an iterable (typically a vector or tuple) of callables (typically functions or interpolators) with length equal to the number of filters contained in the elements ofmags
. This iterable must contain callables that, when called with the associated magnitudes frommags
, will return the expected 1-σ photometric error at that magnitude. The organization is such that the photometric error for stari
in bandj
isσ_ij = errfuncs[j](mags[i][j])
.completefuncs
: an iterable (typically a vector or tuple) of callables (typically functions or interpolators) with length equal to the number of filters contained in the elements ofmags
. This iterable must contain callables that, when called with the associated magnitudes frommags
, will return the probability that a star with that magnitude in that band will be found in your color-magnitude diagram (this should include the original detection probability and any post-detection quality, morphology, or other cuts). The organization is such that the detection probability for stari
in bandj
isc_ij = completefuncs[j](mags[i][j])
.
Keyword Arguments
rng::AbstractRNG=Random.default_rng()
: The object to use for random number generation.ret_idxs::Bool
: whether to return the indices of the inputmags
for the stars that were successfully "observed" and are represented in the outputnew_mags
.
Returns
new_mags
: an object similar tomags
(i.e., aVector{Vector{<:Number}}
,Vector{SVector{N,<:Number}}
, etc.) containing the magnitudes of the mock-observed stars. This will be shorter than the providedmags
vector as we are modelling photometric incompleteness, and the magnitudes will also have random photometric errors added to them. This can be reinterpreted as a 2-dimensionalMatrix
withreduce(hcat,new_mags)
.good_idxs
: ifret_idxs
istrue
, the vector of indices into the inputmags
for the stars that were successfully "observed" and are represented in the outputnew_mags
.
Notes
- This is a simple implementation that seeks to show a simple example of how one can post-process catalogs of "pure" stars from methods like
generate_stars_mass
andgenerate_stars_mag
to include observational effects. This method assumes Gaussian photometric errors, which may not, in general, be accurate. It also assumes that the total detection probability can be modelled as the product of the single-filter detection probabilities as computed bycompletefuncs
(i.e., that the completeness functions are separable across filters). This can be a reasonable assumption when you have separate photometric catalogs derived for each filter and you only collate them afterwards, but it is generally not a good assumption for detection algorithms that operate on simultaneously on multi-band photometry – the completeness functions for these types of algorithms are generally not separable.
Developer Internals
StarFormationHistories.ingest_mags
— Functionnew_mags = ingest_mags(mini_vec::AbstractVector, mags::AbstractVector{T}) where {S <: Number, T <: AbstractVector{S}}
new_mags = ingest_mags(mini_vec::AbstractVector, mags::AbstractMatrix{S}) where S <: Number
Reinterprets provided mags
to be in the correct format for input to Interpolations.interpolate
.
Returns
new_mags::Base.ReinterpretArray{StaticArrays.SVector}
: alength(mini_vec)
vector ofStaticArrays.SVectors
containing the same data asmags
but formatted for input toInterpolations.interpolate
.
StarFormationHistories.sort_ingested
— Function(new_mini_vec, new_mags) = sort_ingested(mini_vec::AbstractVector, mags::AbstractVector)
Takes mini_vec
and mags
and ensures that mini_vec
is sorted (sometimes in PARSEC isochrones they are not) and calls Interpolations.deduplicate_knots!
on mini_vec
to ensure there are no repeat entries. Arguments must satisfy length(mini_vec) == length(mags)
.
StarFormationHistories.mass_limits
— Function(mmin, mmax) = mass_limits(mini_vec::AbstractVector{<:Number}, mags::AbstractVector{T},
mag_names::AbstractVector{String}, mag_lim::Number,
mag_lim_name::String) where T <: AbstractVector{<:Number}
Calculates initial mass limits that reflect a given faint-end magnitude limit.
Arguments
mini_vec::AbstractVector{<:Number}
: a lengthnstars
vector containing initial stellar masses.mags::AbstractVector{<:AbstractVector{<:Number}}
: a lengthnstars
vector, with each element being a lengthnfilters
vector giving the magnitudes of each star in the filtersmag_names
.mag_names::AbstractVector{String}
: a vector giving the names of each filter as strings.mag_lim::Number
: the faint-end magnitude limit you wish to use.mag_lim_name::String
: the name of the filter in whichmag_lim
is to be applied. Must be contained inmag_names
.
Returns
mmin::eltype(mini_vec)
: the initial mass corresponding to your requestedmag_lim
in the filtermag_lim_name
. If all stars provided are brighter than your requestedmag_lim
, then this will be equal tominimum(mini_vec)
.mmax::eltype(mini_vec)
: the maximum valid mass inmini_vec
; simplymaximum(mini_vec)
.
Examples
julia> mass_limits([0.05,0.1,0.2,0.3], [[4.0],[3.0],[2.0],[1.0]], ["F090W"], 2.5, "F090W")
(0.15, 0.3)
julia> mass_limits([0.05,0.1,0.2,0.3], [[4.0,3.0],[3.0,2.0],[2.0,1.0],[1.0,0.0]], ["F090W","F150W"], 2.5, "F090W")
(0.15, 0.3)