def dadt(self): """GW-Driven Hardening and Eccentricity evolution. Returns ------- dadt : (N,) scalar Derivative of semi-major axis vs. time (negative). decdt : scalar or (N,) scalar Derivative of eccentricity vs. time (negative). taus : (N,) scalar Hardening timescale (a / da/dt) due to GW emission. """ m1 = self._evolver.m1[:, np.newaxis] m2 = self._evolver.m2[:, np.newaxis] rads = self._evolver.rads[np.newaxis, :] # eccs = 0.0 print(m1.shape, m2.shape, rads.shape) print(zmath.stats_str(m1 / MSOL)) print(zmath.stats_str(m2 / MSOL)) dadt = -_CONST_DIMENS * (64.0 / 5.0) * m1 * m2 * (m1 + m2) * np.power( rads, -3.0) ''' decdt = np.zeros_like(dadt) # Only bother with the eccentricity terms if non-zero if np.any(eccs > 0.0): e2 = np.square(eccs) ecc_fact_a = (1 + (73.0/24)*e2 + (37.0/96)*e2*e2)*np.power((1.0-e2), -3.5) ecc_fact_e = (eccs + (121.0/304)*eccs*e2)*np.power((1.0-e2), -2.5) decdt = -_CONST_DIMENS * (304.0/15.0) * m1 * m2 * (m1+m2) * np.power(seps, -4.0) dadt *= ecc_fact_a decdt *= ecc_fact_e ''' self.check_timescale("GW", None, 1e-2 * PC, extr=[1e4, 1e12], dadt=dadt) # taus = -rads/dadt # return dadt, decdt, taus return dadt
def init_mass_profile_arrays(self): # SF_EFF_FRAC = 0.2 # RMAX_FACT = 5.0 PROFILE_POWLAW_MAX = -0.1 # PROFILE_POWLAW_MAX = -2.9 PROFILE_POWLAW_MIN = -2.9 verbose = self._verbose # names = self._names if verbose: print("Running `EvolveLZK.init_mass_profile_arrays()") rads = self.rads vols = 4 * np.pi * np.power(rads, 3) / 3 dv = np.concatenate(([vols[0]], vols[1:] - vols[:-1])) # dv = 4*np.pi*np.power(rads, 2) * np.concatenate(([rads[0]], np.diff(rads))) for ii, pt in enumerate(self.PARTICLE_NAMES): aa = self._dens_prof_norms[ii] gg = self._dens_prof_gammas[ii] gg = np.clip(gg, PROFILE_POWLAW_MIN, PROFILE_POWLAW_MAX) dens = aa[:, np.newaxis] * np.power(rads[np.newaxis, :] / PC, -gg[:, np.newaxis]) mass = np.cumsum(dens * dv[np.newaxis, :], axis=-1) # mass.append(_mass) dvar = "dens_" + pt mvar = "mass_" + pt if verbose: print("Setting `{}`, `{}`".format(dvar, mvar)) setattr(self, dvar, dens) setattr(self, mvar, mass) warnings.warn("Using fixed velocity dispersion!") self.vdisp = self.gal_vdisp[:, np.newaxis] * np.ones_like(rads)[ np.newaxis, :] print("vdisp[pc] = " + zmath.stats_str(self.vdisp[:, self._rad_ind], log=False)) return
def check_timescale(self, name, tau, rad=PC, extr=None, dadt=None): EXTR = [1e6, 1e16] if extr is None: extr = EXTR else: for ii in range(2): extr[ii] = EXTR[ii] if extr[ii] is None else extr[ii] if tau is None: tau = -self._evolver.rads / dadt # rad_ind = self._evolver._rad_ind rad_ind = zmath.argnearest(self._evolver.rads, rad) rad_rad = self._evolver.rads[rad_ind] / PC stats = zmath.stats_str(tau[:, rad_ind] / YR, log=False) print(name + " T[{:.0e} pc]/YR = ".format(rad_rad) + stats) tau_med = np.median(tau[:, rad_ind] / YR) if (tau_med < extr[0]) or (tau_med > extr[1]): err = name + " timescale looks wrong! (vs. {:.1e}, {:.1e} [yr])".format( *extr) raise ValueError(err) return
def calc_dmdt_for_details(core=None): """Calculate mass-differences as estimate for accretion rates, add/overwrite in HDF5 files. """ core = Core.load(core) log = core.log cosmo = core.cosmo NUM_SNAPS = core.sets.NUM_SNAPS CONV_ILL_TO_CGS = core.cosmo.CONV_ILL_TO_CGS # log.warning("WARNING: testing `calc_dmdt_for_details`!") # for snap in [135]: for snap in core.tqdm(range(NUM_SNAPS)): fname = core.paths.fname_details_snap(snap) log.debug("Snap {}: '{}'".format(snap, fname)) with h5py.File(fname, 'a') as data: scales = data[DETAILS.SCALES] if scales.size == 0: continue # These are already sorted by ID and scale-factor, so contiguous and chronological # for each BH masses = data[DETAILS.MASSES][:] * CONV_ILL_TO_CGS.MASS mdots = data[DETAILS.MDOTS] u_inds = data[DETAILS.UNIQUE_INDICES] u_counts = data[DETAILS.UNIQUE_COUNTS] # Calculate mass-differences dmdts = np.zeros_like(masses) count = 0 count_all = 0 for ii, nn in zip(u_inds, u_counts): j0 = slice(ii, ii + nn - 1) j1 = slice(ii + 1, ii + nn) # t0 = cosmo.a_to_tage(scales[j0]) # t1 = cosmo.a_to_tage(scales[j1]) z0 = cosmo._a_to_z(scales[j0]) z1 = cosmo._a_to_z(scales[j1]) t0 = cosmo.age(z0).cgs.value t1 = cosmo.age(z1).cgs.value m0 = masses[j0] m1 = masses[j1] dm = m1 - m0 dt = t1 - t0 ss = np.ones_like(dm) neg = (dm < 0.0) | (dt < 0.0) ss[neg] *= -1 inds = (dt != 0.0) dmdts[j1][inds] = ss[inds] * np.fabs(dm[inds] / dt[inds]) count += np.count_nonzero(inds) count_all += inds.size dmdts = dmdts / CONV_ILL_TO_CGS.MDOT log.info("dM/dt nonzero : " + zmath.frac_str(np.count_nonzero(dmdts), masses.size)) log.info("mdots : " + zmath.stats_str(mdots, filter='>')) log.info("dmdts : " + zmath.stats_str(dmdts, filter='>')) return
def interp_bad_grid_vals(grid, grid_temps, grid_valid): grid_temps = np.copy(grid_temps) bads = grid_valid & np.isclose(grid_temps, 0.0) shape = [len(gg) for gg in grid] logging.warning("Fixing bad values: {}".format(zmath.frac_str(bads))) neighbors = [] good_neighbors = [] bads_inds = np.array(np.where(bads)).T for bad in tqdm.tqdm(bads_inds): nbs = [] # print(bad) cnt = 0 for dim in range(4): for side in [-1, +1]: test = [bb for bb in bad] test[dim] += side if test[dim] < 0 or test[dim] >= shape[dim]: continue test = tuple(test) # print("\t", test) # print("\t", temps[test]) nbs.append(test) if grid_temps[test] > 0.0: cnt += 1 neighbors.append(nbs) good_neighbors.append(cnt) num_nbs = [len(nbs) for nbs in neighbors] logging.warning("All neighbors: {}".format(zmath.stats_str(num_nbs))) logging.warning("Good neighbors: {}".format( zmath.stats_str(good_neighbors))) goods = np.zeros(len(neighbors)) MAX_TRIES = 10 still_bad = list(np.argsort(good_neighbors)[::-1]) tries = 0 while len(still_bad) > 0 and tries < MAX_TRIES: keep_bad = [] for kk, ii in enumerate(still_bad): values = np.zeros(num_nbs[ii]) for jj, nbr in enumerate(neighbors[ii]): values[jj] = grid_temps[nbr] cnt = np.count_nonzero(values) if cnt == 0: keep_bad.append(kk) continue new = np.sum(np.log10(values[values > 0])) / cnt loc = tuple(bads_inds[ii]) # print("\t", loc, new, cnt) grid_temps[loc] = 10**new goods[ii] = cnt still_bad = [still_bad[kk] for kk in keep_bad] num_still = len(still_bad) logging.warning("Try: {}, still_bad: {}".format(tries, num_still)) if (tries + 1 >= MAX_TRIES) and (num_still > 0): logging.error("After {} tries, still {} bad!!".format( tries, num_still)) tries += 1 logging.warning("Filled neighbors: {}".format(zmath.stats_str(goods))) logging.warning("Full temps array: {}".format( zmath.stats_str(grid_temps[grid_valid]))) return grid_temps
def __init__(self, fname, e_0=0.0, verbose=False): # names = ['dm', 'gas', 'star'] # self._names = names num_steps = self.NUM_STEPS input_data = np.genfromtxt(fname, names=True, dtype=None) # warnings.warn("\n!!!!DOWNSAMPLING!!!!\n") # NUM = 1000 # inds = np.random.choice(input_data.shape[0], NUM, replace=False) # input_data = input_data[inds] self.z = input_data['redshift'] self._input_data = input_data self._verbose = verbose self._cosmo = cosmopy.get_cosmology() if verbose: print("input_data.shape = {}".format(input_data.shape)) print("names = {}".format(", ".join(input_data.dtype.names))) keys_masses = ['mass_new_prev_in', 'mass_new_prev_out'] masses = np.array([input_data[kk] for kk in keys_masses]).T * MSOL self.masses = masses keys_snaps = ['snap_prev_in', 'snapshot_prev_out', 'snapshot_fin_out'] self.snaps = np.array([input_data[kk] for kk in keys_snaps]).T # keys_gammas = ['dm_gamma', 'gas_gamma', 'star_gamma'] # self.gammas = np.array([-input_data[kk + "_gamma"] for kk in names]).T # m2, m1 = masses.T # mt = m1 + m2 # mr = m2/m1 # self.m1 = masses.max(axis=-1) self.m2 = masses.min(axis=-1) mt = self.m1 + self.m2 self.mtot = mt mr = self.m2 / self.m1 self.mrat = mr self.redz_form = input_data['redshift'] self.tage_form = self._cosmo.age(self.redz_form).cgs.value self.sep0 = input_data['separation'] * 1000 * PC self.gal_vdisp = input_data['vel_disp_fin_out'] * 1e5 self.gal_mstar = input_data['stellar_mass_fin_out'] * MSOL self.gal_mtot = input_data['total_mass_fin_out'] * MSOL # self.dens_prof_star = [input_data['star_norm'] * MSOL, input_data['star_gamma']] # self.dens_prof_gas = [input_data['gas_norm'] * MSOL, input_data['gas_gamma']] # self.dens_prof_dm = [input_data['dm_norm'] * MSOL, input_data['dm_gamma']] # These are for radii in units of 1 pc, and gamma is the negative value norms = [ input_data[nn + "_norm"] * _DENS_CONV for nn in self.PARTICLE_NAMES ] gammas = [input_data[nn + "_gamma"] for nn in self.PARTICLE_NAMES] for ii in range(len(norms)): bads = (norms[ii] < 0.0) num_bad = np.count_nonzero(bads) if num_bad > 0: num_all = bads.size frac = num_bad / num_all reset_perc = 10.0 reset_val = np.percentile(norms[ii][~bads], reset_perc) print("BAD ", self.PARTICLE_NAMES[ii], " {}/{} = {}".format(num_bad, num_all, frac)) print("\tresetting to {:.1f}%ile value: {:.1e}".format( reset_perc, reset_val)) norms[ii][bads] = reset_val self._dens_prof_norms = np.array(norms) self._dens_prof_gammas = np.array(gammas) print("_dens_prof_norms.shape = ", self._dens_prof_norms.shape) # self.mdot = zastro.eddington_accretion(mt) # warnings.warn("CHECK UNITS OF MDOT") # Illustris units ===> g/s self.mdot = input_data['mdot_sum'] * (1e10 * MSOL) / (0.978 * GYR) self.eccen = e_0 self.num_binaries = self.m1.size self.num_steps = num_steps self._shape = (self.num_binaries, self.num_steps) # These are all in CGS self.rad_isco = 3 * radius_schwarzschild(self.m2) sep_extr = [self.rad_isco.min(), self.sep0.max()] # print("sep_extr = ", sep_extr, np.array(sep_extr)/PC) self.rads = np.logspace(*np.log10(sep_extr), self.NUM_STEPS) # Binary circular velocity self.vcirc = vel_circ(mt[:, np.newaxis], mr[:, np.newaxis], self.rads[np.newaxis, :]) rad_ind = zmath.argnearest(self.rads, PC) self._rad_ind = rad_ind if verbose: print("Mtot = " + zmath.stats_str(mt, log=True)) print("Mrat = " + zmath.stats_str(mr, log=False)) print("redz_form = " + zmath.stats_str(self.redz_form, log=False)) print("sepa = " + zmath.stats_str(self.sep0, log=False)) print("gal_mstar = " + zmath.stats_str(self.gal_mstar, log=True)) print("gal_mtot = " + zmath.stats_str(self.gal_mtot, log=True)) print("vcirc[pc] = " + zmath.stats_str(self.vcirc[:, rad_ind], log=False)) print("Mdot = " + zmath.stats_str(self.mdot, log=False)) fedd = self.mdot / (mt * 7e-15) print("fedd = " + zmath.stats_str(fedd, log=False)) # for gg, nn in zip(self.gammas.T, names): # print("{} = ".format(nn) + zmath.stats_str(gg)) for ii, pn in enumerate(self.PARTICLE_NAMES): aa = self._dens_prof_norms[ii] gg = self._dens_prof_gammas[ii] print("{}".format(pn)) print("\tnorms = " + zmath.stats_str(aa)) print("\tgammas = " + zmath.stats_str(-gg)) self.init_mass_profile_arrays() self.init_integral_arrays() self.calc_critical_radii() return
def calculate_timescale(self): # time_coal = 0.0 eccen_final = 0.0 verbose = self._verbose if verbose: print("EvolveLZK.calculate_timescale()") self.init_integral_arrays() self.integrate() rads = self.rads rads_2d = rads[np.newaxis, :] dadt = self.dadt num_binaries, num_rads = dadt.shape dr = np.concatenate((np.diff(rads), [0.0])) * np.ones_like(dadt) alive = (self.rad_isco[:, np.newaxis] <= rads_2d) & (rads_2d <= self.sep0[:, np.newaxis]) # Calculate the durations for normal, mid-evolution steps times = np.zeros_like(dadt) inds = (dadt != 0.0) inds[:, -1] = False times[inds] = -dr[inds] / dadt[inds] # Calculate first step duration # ----------------------------------------------- # Find the last valid index (largest separation) for each binary # `argmax` returns the first index, so reverse it to get the last index beg = np.argmax(np.flip(alive, axis=-1), axis=-1) # (un)reverse the index numbers beg = (num_rads - 1) - beg dr_beg = self.sep0 - rads[beg] times[:, beg] = -dr_beg / dadt[:, beg] # Calculate last step duration # ----------------------------------------------- ''' # NOTE: this will always be a tiny duration... so just ignore it # Find the first valid index (smallest separation) for each binary end = np.argmax(alive) # Then go one index lower, minimum 0 (binary with r[0] == ISCO) end = np.maximum(end - 1, 0) # Set 'alive' values to True for this extra piece of a step dr_end = rads[end] - self.rad_isco times[end] = - dr_end / dadt[end] ''' # Find cumulative lifetimes durs = np.sum(times * alive, axis=-1) # Find coalescence redshift tage_coal = self.tage_form + durs redz_coal = self._cosmo.tage_to_z(tage_coal) inds_coal = np.isfinite(redz_coal) num_coal = np.count_nonzero(inds_coal) frac = num_coal / num_binaries if verbose: print("EvolveLZK.calculate_timescale()") print("Lifetimes: " + zmath.stats_str(durs / GYR) + " [Gyr]") print("Coalescence redshifts: " + zmath.stats_str(redz_coal)) print("\t(valid) Coalescence redshifts: " + zmath.stats_str(redz_coal[redz_coal >= 0.0])) print("{}/{} = {:.4f} coalesce before z=0".format( num_coal, num_binaries, frac)) self.times = times self.durs = durs self.tage_coal = tage_coal self.redz_coal = redz_coal self.inds_coal = inds_coal self.m1 = self.m1 / MSOL # changed from original version to return mass in solar masses self.m2 = self.m2 / MSOL # changed from original version to return mass in solar masses durs = durs / ( ct.Julian_year ) # changed from original version so that it returns in years return durs, eccen_final