Skip to content

Commit f50cb6b

Browse files
authored
Merge pull request #40 from ZeusCosmo/implement_max_SFR_physical
Implement max sfr physical
2 parents b1285f2 + 3e643ac commit f50cb6b

3 files changed

Lines changed: 91 additions & 4 deletions

File tree

tests/test_UVLFs.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,67 @@ def test_UVLF_binned():
138138
assert uvlf_nodust.shape == (3,)
139139

140140
# Without dust, we expect different values than with dust
141-
assert not np.array_equal(uvlf, uvlf_nodust)
141+
assert not np.array_equal(uvlf, uvlf_nodust)
142+
143+
144+
def test_UVLF_binned_with_min_t_formation():
145+
"""Test that min_t_formation_Myr produces finite outputs and suppresses the bright end.
146+
147+
When sigmaUV is large, scatter can push small halos into unphysically bright bins.
148+
Setting min_t_formation_Myr places a physical upper limit on each halo's SFR based on
149+
its maximum stellar mass (all baryons converted to stars) and the minimum formation time.
150+
This should suppress the very bright end of the UVLF without affecting the faint end.
151+
"""
152+
UserParams = zeus21.User_Parameters()
153+
CosmoParams_input = zeus21.Cosmo_Parameters_Input(kmax_CLASS=10., zmax_CLASS=20.)
154+
ClassyCosmo = zeus21.runclass(CosmoParams_input)
155+
CosmoParams = zeus21.Cosmo_Parameters(UserParams, CosmoParams_input, ClassyCosmo)
156+
HMFintclass = zeus21.HMF_interpolator(UserParams, CosmoParams, ClassyCosmo)
157+
158+
# Use a large sigmaUV to create unphysical scatter into the bright end
159+
large_sigmaUV = 2.0
160+
min_t_Myr = 10.0
161+
162+
# AstroParams with the physicality cutoff applied
163+
AstroParams_cut = zeus21.Astro_Parameters(
164+
UserParams, CosmoParams,
165+
sigmaUV=large_sigmaUV,
166+
min_t_formation_Myr=min_t_Myr
167+
)
168+
169+
# AstroParams without the cutoff (default None)
170+
AstroParams_nocut = zeus21.Astro_Parameters(
171+
UserParams, CosmoParams,
172+
sigmaUV=large_sigmaUV
173+
)
174+
175+
z_center = 6.0
176+
z_width = 0.5
177+
# Include a very bright bin (-25) where small-halo scatter is cut off,
178+
# a typical bin (-20), and a faint bin (-15) that should be unaffected
179+
MUV_centers = np.array([-25.0, -20.0, -15.0])
180+
MUV_widths = np.full_like(MUV_centers, 1.0)
181+
182+
uvlf_cut = UVLF_binned(
183+
AstroParams_cut, CosmoParams, HMFintclass,
184+
z_center, z_width, MUV_centers, MUV_widths,
185+
DUST_FLAG=False, RETURNBIAS=False
186+
)
187+
uvlf_nocut = UVLF_binned(
188+
AstroParams_nocut, CosmoParams, HMFintclass,
189+
z_center, z_width, MUV_centers, MUV_widths,
190+
DUST_FLAG=False, RETURNBIAS=False
191+
)
192+
193+
# Output must be finite (no NaNs or Infs) with the cutoff applied
194+
assert np.all(np.isfinite(uvlf_cut)), "UVLF with min_t_formation_Myr cutoff contains NaN or Inf values"
195+
196+
# All values must be non-negative
197+
assert np.all(uvlf_cut >= 0.0), "UVLF with min_t_formation_Myr cutoff contains negative values"
198+
199+
# The cutoff should suppress the very bright end: small halos that could not
200+
# physically produce MUV=-25 galaxies (min_MUV~-18.7 for 1e8 Msun with t_min=10 Myr)
201+
# no longer contribute via scatter, so the bright-end UVLF should be lower
202+
assert uvlf_cut[0] < uvlf_nocut[0], (
203+
"min_t_formation_Myr cutoff should suppress the very bright end (MUV=-25) of the UVLF"
204+
)

zeus21/UVLFs.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,27 @@ def UVLF_binned(Astro_Parameters,Cosmo_Parameters,HMF_interpolator, zcenter, zwi
8080

8181
xhi = np.subtract.outer(MUVcuthi, currMUV)/(np.sqrt(2) * sigmaUV)
8282
xlo = np.subtract.outer(MUVcutlo, currMUV )/(np.sqrt(2) * sigmaUV)
83-
weights = (erf(xhi) - erf(xlo)).T/(2.0 * MUVwidths)
83+
84+
if (Astro_Parameters.min_t_formation_Myr == None):
85+
min_MUV = -100.0 # essentially no cutoff, since the scatter is large at low masses and can cause numerical issues if we try to integrate over unphysically bright galaxies there. This is just a numerical cutoff, not a physical one, and the exact value doesn't matter much since the scatter is large there anyway.
86+
else:
87+
Mstarmax = HMF_interpolator.Mhtab * Cosmo_Parameters.OmegaB /Cosmo_Parameters.OmegaM #max stellar mass in each halo, if all baryons turned to stars
88+
_tmaxSFR = Astro_Parameters.min_t_formation_Myr * 1e6 #arbitrary timescale to determine max SFR in yrs
89+
SFRmax = Mstarmax / (_tmaxSFR)
90+
min_MUV = MUV_of_SFR(SFRmax, Astro_Parameters._kappaUV) #min MUV in each halo, if all baryons turned to stars at max SFR for 10 Myr. This is a very rough cutoff to avoid unphysically small MUVs (bright galaxies) at low masses, which can cause numerical issues since the scatter is large there. It's not a physical cutoff, just a numerical one. The exact value doesn't matter much since the scatter is large there anyway, but it prevents the code from trying to integrate over unphysically bright galaxies in low-mass halos.
91+
x_min = (min_MUV - currMUV)/(np.sqrt(2) * sigmaUV)
92+
xhi_cut = np.fmax(xhi, x_min)
93+
xlo_cut = np.fmax(xlo, x_min)
94+
95+
weights_unnormalized = (erf(xhi_cut) - erf(xlo_cut)).T/(2.0 * MUVwidths)
96+
weights = weights_unnormalized/ (0.5*(1-erf(x_min)+1e-6))[:,None] # Renormalize distributions based on the portion cut off by min_MUV
97+
98+
### Standard as usual, no cuts:
99+
# weights = (erf(xhi) - erf(xlo)).T/(2.0 * MUVwidths) #comment to myself, this 2 in denominator is correct here, nothing to do with the MUVwidths/2 a few lines above
84100

85101
UVLF_filtered = np.trapezoid(weights.T * HMFcurr, HMF_interpolator.Mhtab, axis=-1)
86102

103+
87104
if(Astro_Parameters.USE_POPIII==False):
88105
return UVLF_filtered
89106
else:

zeus21/inputs.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ def __init__(self, UserParams, Cosmo_Parameters,
270270
A_vcb = 1.0,
271271
beta_vcb = 1.8,
272272

273-
quadratic_SFRD_lognormal = False # Sarah Libanore, use second order in lognormal
273+
quadratic_SFRD_lognormal = False, # Sarah Libanore, use second order in lognormal
274+
min_t_formation_Myr = None
274275

275276
):
276277

@@ -412,7 +413,13 @@ def __init__(self, UserParams, Cosmo_Parameters,
412413
if Cosmo_Parameters.Flag_emulate_21cmfast:
413414
print('Quadratic SFRD not yet implemented when Flag_emulate_21cmfast = True; the code will use quadratic_SFRD_lognormal = False')
414415
self.quadratic_SFRD_lognormal = False
415-
416+
417+
if min_t_formation_Myr is not None:
418+
if (not np.isscalar(min_t_formation_Myr)
419+
or not np.isfinite(min_t_formation_Myr)
420+
or min_t_formation_Myr <= 0):
421+
raise ValueError("min_t_formation_Myr must be None or a strictly positive finite number.")
422+
self.min_t_formation_Myr = min_t_formation_Myr #Minimum formation time of galaxies in Myr for UVLF, sets a minimum M*dot = M*/t_formation with fstar = 1
416423

417424

418425
def SED_XRAY(self, En, pop = 0): #pop set to zero as default, but it must be set to either 2 or 3

0 commit comments

Comments
 (0)