Skip to content

Commit

Permalink
Add support for RandomBinaryPairs binary model to `partial_cmd_smoo…
Browse files Browse the repository at this point in the history
…th` (#42)

Major update that implements the RandomBinaryPairs binary model in the smooth template creation process of partial_cmd_smooth. The binaries branch has some additional code where I tried to add BinaryMassRatio but that turned out to be tricky so giving up on trying to get it into the initial release.
  • Loading branch information
cgarling authored Jul 10, 2024
1 parent c4716bd commit 88d7a67
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
doctest(StarFormationHistories)'
- run: sudo apt-get update
- name: Install tex dependencies
run: sudo apt-get install dvipng texlive-latex-extra texlive-fonts-recommended cm-super
run: sudo apt-get install dvipng texlive-latex-base texlive-latex-extra texlive-fonts-recommended cm-super
- name: Make and deploy docs
run: julia --project=docs --color=yes docs/make.jl
env:
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Trapz = "592b5752-818d-11e9-1e9a-2b8ca4a44cd1"
VectorizationBase = "3d5dd08c-fd9d-11e8-17fa-ed2836048c2f"

[weakdeps]
Expand Down Expand Up @@ -58,6 +59,7 @@ StableRNGs = "1"
StaticArrays = "1"
StatsBase = "0.32, 0.33, 0.34"
Test = "<0.0.1, 1"
Trapz = "2"
TypedTables = "1"
VectorizationBase = "0.21"
julia = "1.7"
Expand Down
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ InitialMassFunctions = "e9251ff4-c148-4db3-bf46-89407750fae0"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"

[compat]
Expand All @@ -16,4 +17,5 @@ InitialMassFunctions = "0.1"
Plots = "1"
Printf = "<0.0.1, 1"
PyPlot = "2"
StaticArrays = "1"
StatsBase = "0.34"
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ ENV["MPLBACKEND"] = "agg"
ENV["DOCSBUILD"] = "true"
@info "Running example: smooth_template.jl"
include("../examples/templates/smooth_template.jl")
@info "Running example: smooth_template_binaries.jl"
include("../examples/templates/smooth_template_binaries.jl")
@info "Running example: kernels_example.jl"
include("../examples/templates/kernels_example.jl")
@info "Finished examples"
Expand Down
21 changes: 19 additions & 2 deletions docs/src/binaries.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
# [Multi-Star Systems](@id binaries)
# [Binary Systems](@id binaries)
Here we review the API for including binary systems in our population models. Our [Monte Carlo sampling methods](@ref simulate) supports all three models, while our [smooth template modelling](@ref templates) procedure only supports `NoBinaries` and `RandomBinaryPairs`. A comparison between a Monte Carlo population and a smooth template model for a `RandomBinaryPairs` model with binary fraction of 70% is shown below. The redward shift of the lower main sequence typical of populations with high binary fractions is clearly evident and robustly modelled.

```@example
mv("../../examples/templates/template_compare_binaries.svg", "template_compare_binaries.svg") # hide
mv("../../examples/templates/sigma_distribution_binaries.svg", "sigma_distribution_binaries.svg") # hide
nothing # hide
```

![Comparison of CMD-sampled population with smooth Hess diagram template, with binaries.](template_compare_binaries.svg)

## Types
```@docs
StarFormationHistories.AbstractBinaryModel
StarFormationHistories.sample_system
StarFormationHistories.NoBinaries
StarFormationHistories.RandomBinaryPairs
StarFormationHistories.BinaryMassRatio
```

## Methods
```@docs
StarFormationHistories.binary_system_fraction
StarFormationHistories.binary_number_fraction
StarFormationHistories.binary_mass_fraction
StarFormationHistories.sample_system
```
4 changes: 3 additions & 1 deletion docs/src/examples.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# [Examples](@id examples)

We have constructed an example Jupyter notebook that highlights some common use cases supported by the package. It is recommended that you read some of the background in our main documentation before or concurrently with the example. The notebook is available in the repository as `examples/fitting1.ipynb` and can be viewed in rendered form at [this link](https://nbviewer.org/github/cgarling/StarFormationHistories.jl/blob/main/examples/fitting1.ipynb). It relies on a custom isochrone file which can be made available upon request to the package author.
We have constructed an example Jupyter notebook that highlights some common use cases supported by the package. It is recommended that you read some of the background in our main documentation before or concurrently with the example. The notebook is available in the repository as `examples/fitting1.ipynb` and can be viewed in rendered form at [this link](https://nbviewer.org/github/cgarling/StarFormationHistories.jl/blob/main/examples/fitting1.ipynb). It relies on a custom isochrone file which can be made available upon request to the package author.

There are other scripts in the `examples` directory of the source repository that are used to generate figures for the documentation and provide more targeted examples of usage.
19 changes: 12 additions & 7 deletions examples/templates/smooth_template.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Printf: @sprintf
import PyPlot as plt
import PyPlot: @L_str # For LatexStrings
plt.rc("text", usetex=true)
plt.rc("font", family="serif", serif=["Computer Modern"], size=14)
plt.rc("font", family="serif", serif=["Computer Modern"], size=16)
# This gets close but not quite
# plt.matplotlib.rcParams["axes.formatter.use_mathtext"] = true
# plt.rc("font", family="serif", serif=["cmr10"], size=14)
Expand All @@ -16,7 +16,7 @@ plt.rc("patch", linewidth=1, edgecolor="k", force_edgecolor=true)
# https://matplotlib.org/stable/gallery/images_contours_and_fields/interpolation_methods.html
plt.rc("image", interpolation="none")

# Bool whether to save figure as .svg or not; only save on CI
# Bool whether to save figure as .svg or not; only save when building docs
savefig = ("DOCSBUILD" in keys(ENV)) && (ENV["DOCSBUILD"] == "true")

# Load example isochrone
Expand All @@ -38,11 +38,14 @@ edges = (range(-0.2, 1.2, length=75),
# Set total stellar mass to normalize template to
template_norm = 1e7

# Set binary model to use
binary_model = SFH.NoBinaries()

# Construct error and completeness functions
F090W_complete(m) = SFH.Martin2016_complete(m, 1.0, 28.5, 0.7)
F150W_complete(m) = SFH.Martin2016_complete(m, 1.0, 27.5, 0.7)
F090W_error(m) = min( SFH.exp_photerr(m, 1.03, 15.0, 36.0, 0.02), 0.4)
F150W_error(m) = min( SFH.exp_photerr(m, 1.03, 15.0, 35.0, 0.02), 0.4)
F090W_error(m) = min(SFH.exp_photerr(m, 1.03, 15.0, 36.0, 0.02), 0.4)
F150W_error(m) = min(SFH.exp_photerr(m, 1.03, 15.0, 35.0, 0.02), 0.4)

# Set IMF
imf = Kroupa2001(0.08, 100.0)
Expand All @@ -57,13 +60,13 @@ template = SFH.partial_cmd_smooth(m_ini,
[F090W_complete, F150W_complete];
dmod=distmod,
normalize_value=template_norm,
edges=edges)
edges=edges, binary_model=binary_model)

# Sample analogous population; index [1] is sampled masses, dont need them
starcat_mags = SFH.generate_stars_mass(m_ini, [F090W, F150W],
["F090W", "F150W"], template_norm, imf;
dist_mod=distmod,
binary_model=SFH.NoBinaries())[2]
binary_model=binary_model)[2]

# Model photometric error and incompleteness
obs_mags = SFH.model_cmd(starcat_mags, [F090W_error, F150W_error],
Expand Down Expand Up @@ -215,4 +218,6 @@ ax1.legend(loc="upper right")
if savefig
plt.savefig(joinpath(@__DIR__, "sigma_distribution.svg"), bbox_inches="tight")
end
!plt.isinteractive() && plt.close("all");
if !plt.isinteractive()
plt.close("all");
end
195 changes: 195 additions & 0 deletions examples/templates/smooth_template_binaries.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import StarFormationHistories as SFH
import InitialMassFunctions: Kroupa2001
import DelimitedFiles: readdlm
import Printf: @sprintf

# Set up for plotting
import PyPlot as plt
import PyPlot: @L_str # For LatexStrings
plt.rc("text", usetex=true)
plt.rc("text.latex", preamble="\\usepackage{amsmath}") # for \text
plt.rc("font", family="serif", serif=["Computer Modern"], size=16)
# This gets close but not quite
# plt.matplotlib.rcParams["axes.formatter.use_mathtext"] = true
# plt.rc("font", family="serif", serif=["cmr10"], size=14)
plt.rc("figure", figsize=(5,5))
plt.rc("patch", linewidth=1, edgecolor="k", force_edgecolor=true)
# https://matplotlib.org/stable/gallery/images_contours_and_fields/interpolation_methods.html
plt.rc("image", interpolation="none")

# Bool whether to save figure as .svg or not; only save when building docs
savefig = ("DOCSBUILD" in keys(ENV)) && (ENV["DOCSBUILD"] == "true")

# Load example isochrone
# Path is relative to location of script, so use @__DIR__
isochrone, mag_names = readdlm(joinpath(@__DIR__, "../../data/isochrone.txt"), ' ',
Float64, '\n'; header=true)
# Unpack
m_ini = isochrone[:,1]
F090W = isochrone[:,2]
F150W = isochrone[:,3]

# Set distance modulus for example
distmod = 17.5 # Distance modulus

# Set bins for Hess diagram
edges = (range(0.4, 1.1, length=75),
range(distmod+0.5, distmod+10.0, length=200))

# Set total stellar mass to normalize template to
template_norm = 1e5

# Set binary model to use
binary_model = SFH.RandomBinaryPairs(0.7)

# Construct error and completeness functions
F090W_complete(m) = SFH.Martin2016_complete(m, 1.0, 28.5, 0.7)
F150W_complete(m) = SFH.Martin2016_complete(m, 1.0, 27.5, 0.7)
F090W_error(m) = min(SFH.exp_photerr(m, 1.03, 15.0, 36.0, 0.02), 0.4)
F150W_error(m) = min(SFH.exp_photerr(m, 1.03, 15.0, 35.0, 0.02), 0.4)

# Set IMF
imf = Kroupa2001(0.08, 100.0)
# imf = Kroupa2001(minimum(m_ini), 100.0)
# imf = Kroupa2001(extrema(m_ini)...)

# Construct template
template = SFH.partial_cmd_smooth(m_ini,
[F090W, F150W],
[F090W_error, F150W_error],
2,
[1,2],
imf,
[F090W_complete, F150W_complete];
dmod=distmod,
normalize_value=template_norm,
edges=edges, binary_model=binary_model)

# Sample analogous population
starcat = SFH.generate_stars_mass(m_ini, [F090W, F150W],
["F090W", "F150W"],
template_norm,
imf;
dist_mod=distmod,
binary_model=binary_model)
# binary_model=BinaryMassRatio(0.5, Uniform(0.3,1.0)))
starcat_masses, starcat_mags = starcat # Unpack result

# Model photometric error and incompleteness
obs_mags = SFH.model_cmd(starcat_mags, [F090W_error, F150W_error],
[F090W_complete, F150W_complete])

# Concatenate into 2D matrix
obs_mags = reduce(hcat,obs_mags)

# Make Hess diagram
obs_hess = SFH.bin_cmd(view(obs_mags,1,:) .- view(obs_mags,2,:),
view(obs_mags,2,:), edges=edges).weights

# Residual / σ; sometimes called Pearson residual
signif = (permutedims(obs_hess) .- permutedims(template.weights)) ./
sqrt.(permutedims(template.weights))
signif_plot = copy(signif)
signif_plot[permutedims(obs_hess) .== 0] .= NaN

############################################################################
# Plot

# textx, texty = (0.05, 0.2)
textx, texty = (0.95, 0.83)
ha="right"

fig,axs=plt.subplots(nrows=1, ncols=4, sharex=true, sharey=true, figsize=(20,5))
fig.subplots_adjust(hspace=0.0, wspace=0.0)
# fig.suptitle(@sprintf("Stellar Mass: %.2e M\$_\\odot\$",template_norm))

axs[1].scatter(view(obs_mags,1,:) .- view(obs_mags,2,:), view(obs_mags,2,:),
s=1, marker=".", c="k", alpha=0.1, rasterized=true,
label="CMD-Sampled")
axs[1].text(textx, texty,
@sprintf("a) Sampled CMD\nM\$_*\$ = %.2e M\$_\\odot\$", template_norm),
transform=axs[1].transAxes, va="top", ha=ha)

im1 = axs[3].imshow(permutedims(template.weights), origin="lower",
extent=(extrema(edges[1])..., extrema(edges[2])...),
aspect="auto", cmap="Greys", rasterized=true,
norm=plt.matplotlib.colors.LogNorm(vmin=2.5 +
log10(template_norm/1e7)))
axs[3].text(textx, texty, "c) Smooth Model",
transform=axs[3].transAxes, va="top", ha=ha)

axs[2].imshow(permutedims(obs_hess), origin="lower",
extent=(extrema(edges[1])..., extrema(edges[2])...),
aspect="auto", cmap="Greys", rasterized=true,
norm=plt.matplotlib.colors.LogNorm(vmin=2.5 +
log10(template_norm/1e7),vmax=im1.get_clim()[2]),
label="CMD-Sampled")
axs[2].text(textx, texty, "b) Sampled Hess\nDiagram", transform=axs[2].transAxes,
va="top", ha=ha)

im4 = axs[4].imshow(signif_plot, origin="lower",
extent=(extrema(edges[1])..., extrema(edges[2])...),
aspect="auto", clim=(-2,2), rasterized=true)
axs[4].text(textx, texty, L"d) (Obs - Model) / $\sigma$",
# axs[4].text(textx, texty, L"d) $\frac{(\text{Obs} - \text{Model})}{\sigma}$",
transform=axs[4].transAxes, va="top", ha=ha)

plot_isochrones = true
for i in eachindex(axs)
axs[i].set_xlabel(L"F090W$-$F150W")
if plot_isochrones # & (i != 4) # Don't plot on residual
# axs[i].scatter(F090W .- F150W, F150W .+ distmod, marker=".", c="orange", s=1, alpha=1.0)
axs[i].plot(F090W .- F150W, F150W .+ distmod, c="orange") # , marker=".", markerfacecolor="k")
end
end
axs[1].set_ylabel("F150W")
axs[1].set_ylim(reverse(extrema(edges[2])))
axs[1].set_xlim(extrema(edges[1]))

# fraction prevents too much padding on right
fig.colorbar(im1, ax=axs[1:3], pad=0.005, fraction=0.075)
fig.colorbar(im4, ax=axs[4], pad=0.015)
# println( "sum(obs) - sum(template): ", sum(obs_hess) .- sum(template.weights))
# println( "Difference of sums / sum: ", (sum(obs_hess) .- sum(template.weights)) ./
# sum(obs_hess))
# println( "Sum of residuals: ", sum(abs, permutedims(obs_hess) .-
# permutedims(template.weights)) )
# plt.savefig("template_compare.pdf", bbox_inches="tight")
if savefig
plt.savefig(joinpath(@__DIR__, "template_compare_binaries.svg"), bbox_inches="tight")
end

#################################
# Distribution of σ discrepancies
import Distributions: pdf, Normal, Poisson
import StatsBase: mean, median, std

fig, ax1 = plt.subplots()
hist1 = ax1.hist(filter(isfinite, signif_plot), range=(-3,3), bins=25, density=true)
ax1.set_xlim(extrema(hist1[2]))
ax1.set_xlabel("Residual / Standard Deviation")
ax1.set_ylabel("PDF")
let xplot = first(hist1[2]):0.01:last(hist1[2])
tmpgood = signif |> filter(isfinite) |> filter(x -> first(hist1[2]) <= x <= last(hist1[2]))
tmpgood_p = signif_plot |> filter(isfinite) |> filter(x -> first(hist1[2]) <= x <= last(hist1[2]))
mean_σ = mean(tmpgood_p)
med_σ = median(tmpgood_p)
std_σ = std(tmpgood_p)
ax1.plot(xplot, pdf.(Normal(0.0,1.0),xplot), label="Ideal")
ax1.plot(xplot, pdf.(Normal(mean_σ,std_σ),xplot),
label="Observed", c="magenta")
ax1.axvline(mean_σ, c="k", ls="--")
ax1.text(0.05, 0.95,
# (@sprintf("Median: %.2f\n\$\\sigma\$: %.2f", med_σ, std_σ)),
(@sprintf("Mean: %.2f\n\$\\sigma\$: %.2f", mean_σ, std_σ)),
# (@sprintf("Mean: %.2f\nMedian: %.2f\n\$\\sigma\$: %.2f", mean_σ, med_σ, std_σ)),
transform=ax1.transAxes, va="top", ha="left")
end
ax1.legend(loc="upper right")
# plt.savefig("sigma_distribution.pdf", bbox_inches="tight")
if savefig
plt.savefig(joinpath(@__DIR__, "sigma_distribution_binaries.svg"), bbox_inches="tight")
end
if !plt.isinteractive()
plt.close("all");
end
Loading

0 comments on commit 88d7a67

Please sign in to comment.